package qy.qyalgotrader.utils;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.NumberFormat;
import java.time.Clock;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.collections4.map.ListOrderedMap;
import org.apache.commons.configuration2.INIConfiguration;
import org.apache.commons.configuration2.builder.fluent.Configurations;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.math3.stat.StatUtils;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.bson.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.HashBasedTable;
import com.google.common.math.DoubleMath;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.Indexes;
import static com.mongodb.client.model.Filters.*;

import qy.jalgotrade.bar.Bar;
import qy.jalgotrade.bar.BasicBar;
import qy.jalgotrade.utils.CommonUtils;
import qy.jalgotrade.utils.CommonUtils.MarketSession;
import qy.jalgotrade.Initializer;
import qy.qyalgotrader.utils.Constants.SSE_SZSE_INDEX_MARKET;
import tech.tablesaw.api.ColumnType;
import tech.tablesaw.api.DateTimeColumn;
import tech.tablesaw.api.DoubleColumn;
import tech.tablesaw.api.Row;
import tech.tablesaw.api.Table;
import tech.tablesaw.columns.Column;
import tech.tablesaw.io.csv.CsvReadOptions;
import tech.tablesaw.selection.Selection;

/**
 * @author c-geo
 *
 */
public final class CommonDataUtils {

	/**
	 *
	 */
	public enum HisDataSource {
		TUSHARE, WIND, ZIPLINE
	}

	private static final Logger logger = LoggerFactory.getLogger(CommonDataUtils.class);

	/**
	 * ${...}/my_quant
	 */
	public static final Path MY_QUANT_BASE_DEFAULT = Paths
			.get(Initializer.APP_DEPLOY_CONFIG.getSection("my_quants").getString("baseDir"));

	/**
	 * ${...}/my_quant/wset
	 */
	public static final Path WSET_BASE_DIR_DEFAULT = Paths
	        .get(Initializer.APP_DEPLOY_CONFIG.getSection("my_quants").getString("wsetBaseDir"));

	/**
	 * ${...}/my_quant/ts_hisdata
	 */
	public static final Path TS_HIS_DATA_BASE_DIR_DEFAULT = Paths
	        .get(Initializer.APP_DEPLOY_CONFIG.getSection("my_quants").getString("tsHisDataBaseDir"));

	/**
	 * ${...}/my_quant/ts_hisdata/tick
	 */
	public static final Path TS_TICK_CSV_BASE_DEFAULT = TS_HIS_DATA_BASE_DIR_DEFAULT.resolve("tick");

	/**
	 * ${...}/my_quant/wind_hisdata
	 */
	public static final Path WIND_HIS_DATA_BASE_DIR_DEFAULT = Paths
			.get(Initializer.APP_DEPLOY_CONFIG.getSection("my_quants").getString("windHisDataBaseDir"));

	/**
	 * ${...}/my_quant/wind_hisdata/tick
	 */
	public static final Path WIND_TICK_CSV_BASE_DEFAULT = WIND_HIS_DATA_BASE_DIR_DEFAULT.resolve("tick");

	/**
	 * ${...}/my_quant/zipline_hisdata
	 */
	public static final Path ZIPLINE_HIS_DATA_BASE_DIR_DEFAULT = Paths
			.get(Initializer.APP_DEPLOY_CONFIG.getSection("my_quants").getString("ziplineHisDataBaseDir"));

	/**
	 * ${...}/my_quant/meta.ini
	 */
	public static INIConfiguration HISDATA_META;

	/**
	 * <pre>
	 * rowKey: windCode;
	 * columns: "wind_code", "sec_name", ...
	 * </pre>
	 */
	public static final com.google.common.collect.Table<String, String, String> SSE_SZSE_CODES = HashBasedTable.create();

	/**
	 * qyalgotrader MongoDB name:
	 */
	private static final String __DB_NAME = "qyalgodb";

	private static final MongoClient __mongoClient = Services.getInstance().getMongoClient();

	// @formatter:off
	/**
	 * SSE (上交所) holidays / SHFE (上期所) holidays (法定节假日前一个交易日无夜盘交易):
	 */
	protected static final List<String> _HOLIDAYS = Arrays.asList(
			// 2005:
			"2005-01-03",
			"2005-02-07", "2005-02-08", "2005-02-09", "2005-02-10", "2005-02-11", "2005-02-14", "2005-02-15",
			"2005-05-02", "2005-05-03", "2005-05-04", "2005-05-05", "2005-05-06",
			"2005-10-03", "2005-10-04", "2005-10-05", "2005-10-06", "2005-10-07",
			// 2006:
			"2006-01-02", "2006-01-03",
			"2006-01-30", "2006-01-31", "2006-02-01", "2006-02-02", "2006-02-03",
			"2006-05-01", "2006-05-02", "2006-05-03", "2006-05-04", "2006-05-05",
			"2006-10-02", "2006-10-03", "2006-10-04", "2006-10-05", "2006-10-06",
			// 2007:
			"2007-01-01", "2007-01-02", "2007-01-03",
			"2007-02-19", "2007-02-20", "2007-02-21", "2007-02-22", "2007-02-23",
			"2007-05-01", "2007-05-02", "2007-05-03", "2007-05-04", "2007-05-07",
			"2007-10-01", "2007-10-02", "2007-10-03", "2007-10-04", "2007-10-05", "2007-12-31",
			// 2008:
			"2008-01-01",
			"2008-02-06", "2008-02-07", "2008-02-08", "2008-02-11", "2008-02-12",
			"2008-04-04", "2008-05-01", "2008-05-02", "2008-06-09", "2008-09-15",
			"2008-09-29", "2008-09-30", "2008-10-01", "2008-10-02", "2008-10-03",
			// 2009:
			"2009-01-01", "2009-01-02",
			"2009-01-26", "2009-01-27", "2009-01-28", "2009-01-29", "2009-01-30",
			"2009-04-06", "2009-05-01", "2009-05-28", "2009-05-29",
			"2009-10-01", "2009-10-02", "2009-10-05", "2009-10-06", "2009-10-07", "2009-10-08",
			// 2010:
			"2010-01-01",
			"2010-02-15", "2010-02-16", "2010-02-17", "2010-02-18", "2010-02-19",
			"2010-04-05", "2010-05-03", "2010-06-14", "2010-06-15", "2010-06-16", "2010-09-22", "2010-09-23", "2010-09-24",
			"2010-10-01", "2010-10-04", "2010-10-05", "2010-10-06", "2010-10-07",
			// 2011:
			"2011-01-03",
			"2011-02-02", "2011-02-03", "2011-02-04", "2011-02-07", "2011-02-08",
			"2011-04-04", "2011-04-05", "2011-05-02", "2011-06-06", "2011-09-12",
			"2011-10-03", "2011-10-04", "2011-10-05", "2011-10-06", "2011-10-07",
			// 2012:
			"2012-01-02", "2012-01-03",
			"2012-01-23", "2012-01-24", "2012-01-25", "2012-01-26", "2012-01-27",
			"2012-04-02", "2012-04-03", "2012-04-04", "2012-04-30", "2012-05-01", "2012-06-22",
			"2012-10-01", "2012-10-02", "2012-10-03", "2012-10-04", "2012-10-05",
			// 2013:
			"2013-01-01", "2013-01-02", "2013-01-03",
			"2013-02-11", "2013-02-12", "2013-02-13", "2013-02-14", "2013-02-15",
			"2013-04-04", "2013-04-05", "2013-04-29", "2013-04-30", "2013-05-01",
			"2013-06-10", "2013-06-11", "2013-06-12", "2013-09-19", "2013-09-20",
			"2013-10-01", "2013-10-02", "2013-10-03", "2013-10-04", "2013-10-07",
			// 2014:
			"2014-01-01",
			"2014-01-31", "2014-02-03", "2014-02-04", "2014-02-05", "2014-02-06",
			"2014-04-07", "2014-05-01", "2014-05-02", "2014-06-02", "2014-09-08",
			"2014-10-01", "2014-10-02", "2014-10-03", "2014-10-06", "2014-10-07",
			// 2015:
			"2015-01-01", "2015-01-02",
			"2015-02-18", "2015-02-19", "2015-02-20", "2015-02-23", "2015-02-24",
			"2015-04-06", "2015-05-01", "2015-06-22", "2015-09-03", "2015-09-04",
			"2015-10-01", "2015-10-02", "2015-10-05", "2015-10-06", "2015-10-07",
			// 2016:
			"2016-01-01",
			"2016-02-08", "2016-02-09", "2016-02-10", "2016-02-11", "2016-02-12",
			"2016-04-04", "2016-05-02", "2016-06-09", "2016-06-10", "2016-09-15", "2016-09-16",
			"2016-10-03", "2016-10-04", "2016-10-05", "2016-10-06", "2016-10-07",
			// 2017:
			"2017-01-02",
			"2017-01-27", "2017-01-30", "2017-01-31", "2017-02-01", "2017-02-02",
			"2017-04-03", "2017-04-04", "2017-05-01", "2017-05-29", "2017-05-30",
			"2017-10-02", "2017-10-03", "2017-10-04", "2017-10-05", "2017-10-06",
			// 2018:
			"2018-01-01",
			"2018-02-15", "2018-02-16", "2018-02-19", "2018-02-20", "2018-02-21",
			"2018-04-05", "2018-04-06", "2018-04-30", "2018-05-01", "2018-06-18", "2018-09-24",
			"2018-10-01", "2018-10-02", "2018-10-03", "2018-10-04", "2018-10-05", "2018-12-31",
			// 2019:
			"2019-01-01",
			"2019-02-04", "2019-02-05", "2019-02-06", "2019-02-07", "2019-02-08",
			"2019-04-05", "2019-05-01", "2019-05-02", "2019-05-03", "2019-06-07", "2019-09-13",
			"2019-10-01", "2019-10-02", "2019-10-03", "2019-10-04", "2019-10-07",
			// 2020:
			"2020-01-01",
			"2020-01-24", "2020-01-27", "2020-01-28", "2020-01-29", "2020-01-30", "2020-01-31",
			"2020-04-06", "2020-05-01", "2020-05-04", "2020-05-05", "2020-06-25", "2020-06-26",
			"2020-10-01", "2020-10-02", "2020-10-05", "2020-10-06", "2020-10-07", "2020-10-08",
			// 2021:
			"2021-01-01",
			"2021-02-11", "2021-02-12", "2021-02-15", "2021-02-16", "2021-02-17",
			"2021-04-05", "2021-05-03", "2021-05-04", "2021-05-05", "2021-06-14", "2021-09-20", "2021-09-21",
			"2021-10-01", "2021-10-04", "2021-10-05", "2021-10-06", "2021-10-07");

	/**
	 * _holiday_year_upper_limit = max([int(e[0:4]) for e in _HOLIDAYS])
	 */
	protected static int _holidayYearUpperLimit; // _holiday_year_upper_limit = max([int(e[0:4]) for e in _HOLIDAYS])

	// 中国期货 (法定节假日前一个交易日等) 不含夜盘的交易日 (T-1 日):
	/**
	 * 上期所 (夜盘交易上线: 2013-07-05):
	 */
	protected static final List<String> _NO_NIGHT_MARKET_SHFE = Arrays.asList(
			// 2013:
			"2013-09-18", "2013-09-30", "2013-12-31",
			// 2014:
			"2014-01-30", "2014-04-04", "2014-04-30", "2014-04-30", "2014-05-30", "2014-09-05", "2014-09-30", "2014-12-31",
			// 2015:
			"2015-02-17", "2015-04-03", "2015-04-30", "2015-06-19", "2015-09-02", "2015-09-25", "2015-09-30", "2015-12-31",
			// 2016:
			"2016-02-05", "2016-04-01", "2016-04-29", "2016-06-08", "2016-09-14", "2016-09-30", "2016-12-30",
			// 2017:
			"2017-01-26", "2017-03-31", "2017-04-28", "2017-05-26", "2017-09-29", "2017-12-29",
			// 2018:
			"2018-02-14", "2018-04-04", "2018-04-27", "2018-06-15", "2018-09-21", "2018-09-28", "2018-12-28",
			// 2019:
			"2019-02-01", "2019-04-04", "2019-04-30", "2019-06-06", "2019-09-12", "2019-09-30");

	/**
	 * 大商所 (夜盘交易上线: 2014-07-04):
	 */
	protected static final List<String> _NO_NIGHT_MARKET_DCE = Arrays.asList(
			// 2014:
			"2014-09-05", "2014-09-30", "2014-12-31",
			// 2015:
			"2015-02-17", "2015-04-03", "2015-04-30", "2015-06-19", "2015-09-02", "2015-09-25", "2015-09-30", "2015-12-31",
			// 2016:
			"2016-02-05", "2016-04-01", "2016-04-29", "2016-06-08", "2016-09-14", "2016-09-30", "2016-12-30",
			// 2017:
			"2017-01-26", "2017-03-31", "2017-04-28", "2017-05-26", "2017-09-29", "2017-12-29",
			// 2018:
			"2018-02-14", "2018-04-04", "2018-04-27", "2018-06-15", "2018-09-21", "2018-09-28", "2018-12-28",
			// 2019:
			"2019-02-01", "2019-04-04", "2019-04-30", "2019-06-06", "2019-09-12", "2019-09-30");

	/**
	 * 郑商所 (夜盘交易上线: 2014-12-12):
	 */
	protected static final List<String> _NO_NIGHT_MARKET_CZCE = Arrays.asList(
			// 2014:
			"2014-12-31",
			// 2015:
			"2015-02-17", "2015-04-03", "2015-04-30", "2015-06-19", "2015-09-02", "2015-09-25", "2015-09-30", "2015-12-31",
			// 2016:
			"2016-02-05", "2016-04-01", "2016-04-29", "2016-06-08", "2016-09-14", "2016-09-30", "2016-12-30",
			// 2017:
			"2017-01-26", "2017-03-31", "2017-04-18", "2017-04-28", "2017-05-26", "2017-09-29", "2017-12-29",
			// 2018:
			"2018-02-14", "2018-04-04", "2018-04-27", "2018-06-15", "2018-09-21", "2018-09-28", "2018-12-28",
			// 2019:
			"2019-02-01", "2019-04-04", "2019-04-30", "2019-06-06", "2019-09-12", "2019-09-30");
	// @formatter:on

	// 标的代码正则表达式 patterns:
	/**
	 * 通用 WindCode: "([-\\w\\(\\+\\)]+)\\.(\\w+)"
	 */
	public static final Pattern PATTERN_FULL_WC = Pattern.compile("([-\\w\\(\\+\\)]+)\\.(\\w+)");

	/**
	 * 中国 A 股 (沪深证券交易所) code: "(\\d{6})"
	 */
	public static final Pattern PATTERN_FULL_CN_STOCK_A_CODE = Pattern.compile("(\\d{6})");

	/**
	 * 中国期货 code (或 WindCode): "([a-zA-z]+)(\\d*)\\.?(\\w+)?"
	 */
	public static final Pattern PATTERN_FULL_CN_FU_CODE = Pattern.compile("([a-zA-z]+)(\\d*)\\.?(\\w+)?");

	/**
	 * 期货仿真合约 code: "([\\w]+)-S"
	 */
	public static final Pattern PATTERN_FULL_CN_FU_SIM_CODE = Pattern.compile("([\\w]+)-S");

	static {

		int y, mx = 0;
		for (String e : _HOLIDAYS) {
			y = Integer.parseInt(e.substring(0, 4));
			if (y > mx) {
				mx = y;
			}
		}
		_holidayYearUpperLimit = mx;

		try {
			HISDATA_META = new Configurations().ini(MY_QUANT_BASE_DEFAULT.resolve("meta.ini").toFile());
		} catch (ConfigurationException e) {
			e.printStackTrace();
		}
	}

	/**
	 * @param barDateTime barDateTime
	 * @param lastBar lastBar
	 * @throws Exception Exception
	 * @return Bar
	 */
	public static Bar genEmptyBarBy(ZonedDateTime barDateTime, Bar lastBar) throws Exception {

		double lastClose = lastBar.getClose();
		Map<String, Double> newBarExtras = new HashMap<>();
		Map<String, Double> lastBarExtras = lastBar.getExtraColumns();
		for (String field : lastBarExtras.keySet()) {
			// VWAP, VWAP_INTRADAY: 引用和计算时为避免出现 NaN, 尽管为空 Bar, 仍保持上一个 Bar 的原值, 而不是 0:
			if (Constants.BAR_FIELD_AMOUNT.equals(field)) {
				newBarExtras.put(field, 0.0);
			} else if (Constants.BAR_FIELD_VWAP.equals(field)) {
				newBarExtras.put(field, lastBarExtras.get(field));
			} else if (Constants.BAR_FIELD_VWAP_INTRADAY.equals(field)) {
				newBarExtras.put(field, lastBarExtras.get(field));
			} else if (Constants.BAR_FIELD_PCT_CHANGE_INTRADAY.equals(field)) {
				newBarExtras.put(field, lastBarExtras.get(field));
			} else if (Constants.BAR_FIELD_RANGE_INTRADAY.equals(field)) {
				newBarExtras.put(field, lastBarExtras.get(field));
			} else if (Constants.BAR_FIELD_POSITION.equals(field)) {
				newBarExtras.put(field, lastBarExtras.get(field));
			}
		}
		if (lastBarExtras.containsKey(Constants.BAR_FIELD_AMOUNT)) {
			newBarExtras.put(Constants.BAR_FIELD_AMOUNT, 0.0);
		}
		return new BasicBar(barDateTime, lastClose, lastClose, lastClose, lastClose, 0.0, lastClose,
				lastBar.getFrequency(), newBarExtras);
	}

	/**
	 * 
	 * @param theTdP default: nowTradingDate()
	 * @throws IOException IOException
	 */
	public static void readSseSzseCodes(Optional<LocalDate> theTdP) throws IOException {

		LocalDate theTd = theTdP.orElse(tradingDaysOffset(LocalDate.now(), -1));
		String ym = theTd.format(Constants.DATE_FORMAT_YYYYMM);
		String ymd = theTd.format(Constants.DATE_FORMAT_YYYYMMDD);
		Path stockACsvPath = WSET_BASE_DIR_DEFAULT.resolve(
				String.format("sectorconstituent/stock_a/%s/stock_a_%s.csv", ym, ymd));
		Path tFundsCsvPath = WSET_BASE_DIR_DEFAULT.resolve(
				String.format("sectorconstituent/trade_funds/%s/trade_funds_%s.csv", ym, ymd));
		Table df1 = CommonUtils.readTableFromCsv(stockACsvPath, "GBK");
		if (df1 != null && !df1.isEmpty()) {
			for (int i = 0; i < df1.rowCount(); i++) {
				Row r = df1.row(i);
				String wc = r.getString("wind_code");
				String secName = r.getString("sec_name");
				SSE_SZSE_CODES.put(wc, "wind_code", wc);
				SSE_SZSE_CODES.put(wc, "sec_name", secName);
			}
		}
		Table df2 = CommonUtils.readTableFromCsv(tFundsCsvPath, "GBK");
		if (df2 != null && !df2.isEmpty()) {
			for (int i = 0; i < df2.rowCount(); i++) {
				Row r = df2.row(i);
				String wc = r.getString("wind_code");
				String secName = r.getString("sec_name");
				SSE_SZSE_CODES.put(wc, "wind_code", wc);
				SSE_SZSE_CODES.put(wc, "sec_name", secName);
			}
		}
	}

	/**
	 * 依赖于 _HOLIDAYS
	 * @param dt dt
	 * @return isTradingDay
	 */
	public static boolean isTradingDay(LocalDate dt) {

		if (dt.getYear() > _holidayYearUpperLimit) {
			throw new IllegalArgumentException(String.format("date: %s exceed upper limit!", dt));
		}

		return !Arrays.asList(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY).contains(dt.getDayOfWeek())
				&& !_HOLIDAYS.contains(dt.format(Constants.DATE_FORMAT_STD));
	}

	/**
	 * 适用于 中国 A 股
	 * 
	 * @param wc wc
	 * @param aDate aDate
	 * @return isTradingDayForWindCode
	 */
	public static boolean isTradingDayForWindCode(String wc, LocalDate aDate) {

		if (!isTradingDay(aDate)) {
			return false;
		}

		String collName = "qset.tradesuspend";
		String dtStr = aDate.format(Constants.DATE_FORMAT_STD);

		/*
		 * select *
		 * from "qset.tdays"
		 * where "date" = dtStr,
		 *     "wind_code" = wc:
		 */
		MongoCollection<Document> coll = __mongoClient.getDatabase(__DB_NAME).getCollection(collName);
		FindIterable<Document> curs = coll.find(and(eq("date", dtStr), eq("wind_code", wc)))
		        .hint(and(Indexes.ascending("date"), Indexes.ascending("wind_code")));
		if (curs.first() != null) {
			return curs.first().getString("suspend_type").equals("盘中停牌");
		} else {
			return true;
		}
	}

	/**
	 * 适用于 中国 A 股
	 *
	 * @param wc wc
	 * @param aDate aDate
	 * @param useScStockAInfos 使用 qset.sectorconstituent.stocka
	 * @return isTradingDayForWindCode
	 */
	public static boolean isTradingDayForWindCode(String wc, LocalDate aDate, boolean useScStockAInfos) {

		if  (!useScStockAInfos) {
			return isTradingDayForWindCode(wc, aDate);
		} else {
			if (!isTradingDay(aDate)) {
				return false;
			}

			String collName = "qset.sectorconstituent.stocka";
			String dtStr = aDate.format(Constants.DATE_FORMAT_STD);

			/*
			 * select *
			 * from "qset.sectorconstituent.stocka"
			 * where "date" = dtStr,
			 *     "wind_code" = wc:
			 */
			MongoCollection<Document> coll = __mongoClient.getDatabase(__DB_NAME).getCollection(collName);
			FindIterable<Document> curs = coll.find(and(eq("date", dtStr), eq("wind_code", wc)))
					.hint(and(Indexes.ascending("date"), Indexes.ascending("wind_code")));
			if (curs.first() != null) {
				return Arrays.asList("交易", "盘中停牌").contains(curs.first().getString("TRADE_STATUS"));
			} else {  // TODO: windCode 不存在: 可能存在变更过等情况
				return false;
			}
		}
	}

	/**
	 * 
	 * @param baseDate baseDate
	 * @param offset   可 &lt; 0, 当 &gt; 0 时上限为当年 (受 _holidayYearUpperLimit 限制)
	 * @return tradingDaysOffset
	 */
	public static LocalDate tradingDaysOffset(LocalDate baseDate, int offset) {

		return tradingDaysOffset(baseDate, offset, null);
	}

	/**
	 * TODO: 沪 / 深证券交易所, 四大期货交易所交易日同步?
	 * 
	 * @param baseDate  baseDate
	 * @param offset   可 &lt; 0, 当 &gt; 0 时上限为当年 (受 _holidayYearUpperLimit 限制)
	 * @param wc       支持沪 / 深证券交易所中国A股; 返回值同 w.tdaysoffset():
	 * @return tradingDaysOffset
	 */
	public static LocalDate tradingDaysOffset(LocalDate baseDate, int offset, String wc) {

		List<LocalDate> tds = getTradingDays(baseDate, offset, wc, false);
		if (offset >= 0) {
			return tds.get(tds.size() - 1);
		} else {
			return tds.get(0);
		}
	}

	/**
	 * 
	 * @param baseDate baseDate
	 * @param offset 可 &lt; 0, 当 &gt; 0 时上限为当年 (受 _holidayYearUpperLimit 限制)
	 * @return getTradingDays
	 */
	public static List<LocalDate> getTradingDays(LocalDate baseDate, int offset) {

		return getTradingDays(baseDate, offset, null, false);
	}

	/**
	 * 
	 * @param baseDate baseDate
	 * @param offset 可 &lt; 0, 当 &gt; 0 时上限为当年 (受 _holidayYearUpperLimit 限制)
	 * @param inclZeroTd 若 inclZeroTd 为 False, 则返回不含 起始交易日, 即 offset 为 0 的交易日
	 * @return getTradingDays
	 */
	public static List<LocalDate> getTradingDays(LocalDate baseDate, int offset, boolean inclZeroTd) {
	
		return getTradingDays(baseDate, offset, null, inclZeroTd);
	}

	/**
	 * <pre>
	 * wc: 支持沪 / 深证券交易所中国A股; 返回值同 w.tdays():
	 * </pre>
	 * 
	 * @param baseDate baseDate
	 * @param offset 可 &lt; 0, 当 &gt; 0 时上限为当年 (受 _holidayYearUpperLimit 限制)
	 * @param wcP default: null
	 * @param inclZeroTdP default: false
	 * @return getTradingDays
	 */
	public static List<LocalDate> getTradingDays(LocalDate baseDate, int offset, Optional<String> wcP,
			Optional<Boolean> inclZeroTdP) {

		String wc = wcP.orElse(null);
		boolean inclZeroTd = inclZeroTdP.orElse(false);

		return getTradingDays(baseDate, offset, wc, inclZeroTd);
	}

	/**
	 * <pre>
	 * wc: 支持沪 / 深证券交易所中国A股; 返回值同 w.tdays():
	 * </pre>
	 * 
	 * TODO: 沪 / 深证券交易所, 四大期货交易所交易日同步?
	 * 
	 * @param baseDate baseDate
	 * @param offset 可 &lt; 0, 当 &gt; 0 时上限为当年 (受 _holidayYearUpperLimit 限制)
	 * @param wc default: null
	 * @param inclZeroTd 若 inclZeroTd 为 False, 则返回不含 起始交易日, 即 offset 为 0 的交易日
	 * @return getTradingDays
	 */
	public static List<LocalDate> getTradingDays(LocalDate baseDate, int offset, String wc, boolean inclZeroTd) {

		List<LocalDate> tds = new ArrayList<>();
		int direc = offset != 0 ? offset / Math.abs(offset) : 0;

		LocalDate currDate = baseDate;
		LocalDate startDate = null;
		int ct = 0;
		if (wc == null) {
			// 找到 offset 为 0 的交易日:
			while (!isTradingDay(currDate)) {
				currDate = currDate.minusDays(1);
			}

			startDate = currDate;
			ct = Math.abs(offset);
			tds.add(startDate);
			while (ct > 0) {
				currDate = currDate.plusDays(direc);
				if (isTradingDay(currDate)) {
					tds.add(currDate);
					ct--;
				}
			}
		} else {
			// 找到 offset 为 0 的交易日:
			while (!isTradingDayForWindCode(wc, currDate)) {
				currDate = baseDate.minusDays(1);
			}

			startDate = currDate;
			ct = Math.abs(offset);
			tds.add(startDate);
			while (ct > 0) {
				currDate = currDate.plusDays(direc);
				if (currDate.compareTo(LocalDate.now()) > 0) {
					// 暂不支持获取后推 交易日 超过 系统当前日期: 由于中国 A 股停牌公告等情况:
					throw new IllegalArgumentException(String
					        .format("getTradingDays(%s, %s, %s, %s) iteration beyond today!", baseDate, offset, wc, inclZeroTd));
				}
				if (isTradingDayForWindCode(wc, currDate)) {
					tds.add(currDate);
					ct--;
				}
			}
		}

		if (offset == 0) {
			return tds;
		}

		if (direc < 0) {
			Collections.reverse(tds);
			if (inclZeroTd) {
				return tds;
			} else {
				return tds.subList(0, tds.size() - 1);
			}
		} else {
			if (inclZeroTd) {
				return tds;
			} else {
				return tds.subList(1, tds.size());
			}
		}
	}

	/**
	 * 
	 * @param fromDate fromDate
	 * @param toDate toDate
	 * @return tradingDaysBy
	 */
	public static List<LocalDate> tradingDaysBy(LocalDate fromDate, LocalDate toDate) {

		return tradingDaysBy(fromDate, toDate, null);
	}

	/**
	 * TODO: wc 暂不支持沪深 A 股
	 * 
	 * @param fromDate fromDate
	 * @param toDate toDate
	 * @param wc default: null
	 * @return tradingDaysBy
	 */
	public static List<LocalDate> tradingDaysBy(LocalDate fromDate, LocalDate toDate, String wc) {

		List<LocalDate> ret = new ArrayList<>();
		LocalDate currDt = fromDate;
		while (currDt.compareTo(toDate) <= 0) {
			if (wc == null || wc.length() == 0) {
				if (isTradingDay(currDt)) {
					ret.add(currDt);
				}
			} else {
				if (isTradingDayForWindCode(wc, currDt)) {
					ret.add(currDt);
				}
			}
			currDt = currDt.plusDays(1);
		}
		return ret;
	}

	/**
	 * <p>
	 * 根据现在时间, 判断交易日:
	 * </p>
	 * <p>
	 * 1. 若 T 为交易日, 则上下午盘 (Constants.AM_MKT_TIME_CN_FU (08:00:00) 到 Constants.NIGHT_MKT_TIME_CN_FU (17:30:00))
	 * 时段返回 T;
	 * </p>
	 * <p>
	 * 2. 若 T 为交易日, 则夜盘 (Constants.NIGHT_MKT_TIME_CN_FU (17:30:00) 到 23:59:59.999999) 时段返回 T+1;
	 * </p>
	 * <p>
	 * 3. 若 T 为交易日, 则午夜盘 (00:00:00 到 Constants.AM_MKT_TIME_CN_FU (08:00:00)) 时段返回 T;
	 * </p>
	 * <p>
	 * 4. 若 D 非交易日, 则午夜盘 (00:00:00 到 Constants.AM_MKT_TIME_CN_FU (08:00:00)) 时段返回 D 午夜盘 所属 交易日 T;
	 * </p>
	 * <p>
	 * 4. 若 D 非交易日, 则非午夜盘 (Constants.AM_MKT_TIME_CN_FU (08:00:00) 到 23:59:59.999999) 时段视为复盘, 返回 D 之前最近一个交易日 T-1;
	 * </p>
	 * 
	 * TODO: 是否考虑公共假日无夜盘的情况?
	 * @return nowTradingDate
	 */
	public static LocalDate nowTradingDate() {

		LocalDate today = LocalDate.now();
		LocalTime now = LocalTime.now();
		boolean isTday = isTradingDay(today);
		if (isTday) {
			if (now.compareTo(Constants.AM_MKT_TIME_CN_FU) > 0
					&& now.compareTo(Constants.NIGHT_MKT_TIME_CN_FU) < 0) {
				// 上午盘 / 下午盘:
				return today;
			} else if (now.compareTo(Constants.NIGHT_MKT_TIME_CN_FU) > 0) {
				// 夜盘:
				return tradingDaysOffset(today, 1);
			} else {
				// 午夜盘:
				return today;
			}
		} else {
			// 休息日前一个交易日午夜盘 或 非交易日复盘:
			if (now.compareTo(Constants.AM_MKT_TIME_CN_FU) < 0) {
				// T+1, D+1 午夜盘:
				return tradingDaysOffset(today, 1);
			} else {
				// 非交易日复盘:
				return tradingDaysOffset(today, 0);
			}
		}
	}

	/**
	 * <pre>
	 * <asset-code> "." <exchange-suffix>
	 * e.g.: 601398.SH, 000001.SZ, 2318.HK, JM.DCE, RB.SHF, BABA.N, ...
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @return isWindCode
	 */
	public static boolean isWindCode(String codeOrWc) {

		return PATTERN_FULL_WC.matcher(codeOrWc).matches();
	}

	/**
	 * 判断 windcode 是否为沪深交易所指数:
	 * 
	 * @param wc wc
	 * @return isSseSzseIndex
	 */
	public static boolean isSseSzseIndex(String wc) {

		return Constants.SSE_SZSE_STOCK_MKT_I.rowKeySet().contains(wc);
	}

	/**
	 * e.g.: AF1906-S.CFE (仿真合约):
	 * 
	 * @param codeOrWc codeOrWc
	 * @return isCnFuSimCode
	 */
	public static boolean isCnFuSimCode(String codeOrWc) {

		return PATTERN_FULL_CN_FU_SIM_CODE.matcher(codeOrWc).matches();
	}

	/**
	 * return (&lt;asset-code&gt;, &lt;exchange-suffix&gt;)
	 * 
	 * @param wc wc
	 * @return (&lt;asset-code&gt;, &lt;exchange-suffix&gt;)
	 */
	public static Pair<String, String> windCodeTuple(String wc) {

		Matcher m = PATTERN_FULL_WC.matcher(wc);
		if (!m.matches()) {
			throw new IllegalArgumentException(String.format("Unrecognized windCode: %s !", wc));
		} else {
			return Pair.of(m.group(1), m.group(2));
		}
	}

	/**
	 * e.g.: "IH1904.CFE" -> "IH1904"
	 * 
	 * @param wc wc
	 * @return code
	 */
	public static String windCodeToCode(String wc) {

		Matcher m = PATTERN_FULL_WC.matcher(wc);
		if (!m.matches()) {
			throw new IllegalArgumentException(String.format("Unrecognized windCode: %s !", wc));
		} else {
			return m.group(1);
		}
	}

	/**
	 * TODO: 目前仅支持 沪, 深证券交易所 (不支持指数 code), 中国商品 (不支持 仿真品种):
	 * 
	 * @param codeOrWc codeOrWc
	 * @return WindCode
	 */
	public static String toWindCode(String codeOrWc) {

		if (isWindCode(codeOrWc)) {
			return codeOrWc;
		} else {
			// 沪, 深证券交易所: A 股, 基金, 可转债:
			Matcher m = PATTERN_FULL_CN_STOCK_A_CODE.matcher(codeOrWc);
			if (m.matches()) {
				return cnStockACodeToWindCode(codeOrWc);
			}
			// 中国期货:
			m = PATTERN_FULL_CN_FU_CODE.matcher(codeOrWc);
			if (m.matches()) {
				return cnFuturesCodeToWindCode(codeOrWc);
			}
		}
		throw new IllegalArgumentException(String.format("Unrecognized code: %s !", codeOrWc));
	}

	/**
	 * <pre>
	 * 通过 中国 A 股 (沪深交易所挂牌标的) 代码的前两位, 判断所属交易所:
	 * e.g.: "601398" -> "SSE", "300059" -> "SZSE", "518880" -> "SSE"
	 * </pre>
	 * 
	 * @param code code
	 * @return ExchangeId
	 */
	public static String getExchangeIdByCnStockACode(String code) {

		String cd2 = code.substring(0, 2);
		if (Arrays.asList("60", "68", "50", "51", "58", "11").contains(cd2)) {
			// 沪: A 股, 基金, 可转债:
			return "SSE";
		} else if (Arrays.asList("00", "30", "15", "16", "18", "12").contains(cd2)) {
			// 深: A 股, 基金, 可转债:
			return "SZSE";
		} else {
			throw new IllegalArgumentException(String.format("Can NOT infer exchange for code: %s !", code));
		}
	}

	/**
	 * e.g. "600030" -> "600030.SH"
	 * 
	 * @param code code
	 * @return WindCode
	 */
	public static String cnStockACodeToWindCode(String code) {

		String exchId = getExchangeIdByCnStockACode(code);
		String exchWindSuffix = (String) Constants.EXCHANGE_INFOS_WIND.get(exchId, "exchWindSuffix");
		return String.format("%s.%s", code, exchWindSuffix);
	}

	/**
	 * e.g. "RB1905" -> "SHFE" (不支持 仿真品种)
	 * 
	 * @param codeOrWc codeOrWc
	 * @return ExchangeId
	 */
	public static String getExchangeIdByCnFuturesCode(String codeOrWc) {

		String cnFuName = getCnFuturesCodeName(codeOrWc);
		return (String) Constants.CN_FUTURES_CODES.get(cnFuName, "exchName");
	}

	/**
	 * e.g. "RB1905" -> "RB1905.SHF"
	 * 
	 * @param code code
	 * @return WindCode
	 */
	public static String cnFuturesCodeToWindCode(String code) {

		code = code.toUpperCase();
		String exchId = getExchangeIdByCnFuturesCode(code);
		String exchWindSuffix = (String) Constants.EXCHANGE_INFOS_WIND.get(exchId, "exchWindSuffix");
		return String.format("%s.%s", code, exchWindSuffix);
	}

	/**
	 * e.g. "RB1905.SHF" -> "RB"
	 * 
	 * @param codeOrWc codeOrWc
	 * @return CnFuturesCodeName
	 */
	public static String getCnFuturesCodeName(String codeOrWc) {

		codeOrWc = codeOrWc.toUpperCase();
		Matcher m = PATTERN_FULL_CN_FU_CODE.matcher(codeOrWc);
		if (m.matches()) {
			return m.group(1);
		} else {
			throw new IllegalArgumentException(String.format("Unknown cn futures code (or windCode): %s !", codeOrWc));
		}
	}

	/**
	 * (不支持 仿真品种)
	 * 
	 * @param codeOrWc codeOrWc
	 * @return boolean
	 */
	public static boolean isCnFuturesCodeIlliquidity(String codeOrWc) {

		String cnFuName = getCnFuturesCodeName(codeOrWc);
		return Constants.CN_FUTURES_ILLIQUIDITY_FILTER.rowKeySet().contains(cnFuName);
	}

	/**
	 * (不支持 仿真品种) 若 td 为 None, 则按最新交易时间:
	 * 
	 * @param codeOrWc codeOrWc
	 * @return boolean
	 */
	public static boolean isCnFuturesCodeHasNightMarket(String codeOrWc) {

		return isCnFuturesCodeHasNightMarket(codeOrWc, null);
	}

	/**
	 * (不支持 仿真品种) 若 td 为 None, 则按最新交易时间:
	 * 
	 * @param codeOrWc codeOrWc
	 * @param td default: null
	 * @return boolean
	 */
	public static boolean isCnFuturesCodeHasNightMarket(String codeOrWc, LocalDate td) {

		String cnFuName = getCnFuturesCodeName(codeOrWc);
		if (td == null) {
			return Constants.CN_FUTURES_CODES.get(cnFuName, "openTimeNt") == null
			        && Constants.CN_FUTURES_CODES.get(cnFuName, "closeTimeNt") == null;
		} else {
			@SuppressWarnings("unchecked")
			List<Map<String, Object>> tthList = (List<Map<String, Object>>) Constants.CN_FUTURES_TRADING_TIME
			        .get(cnFuName, "tradingTimeHist");
			for (Map<String, Object> e : tthList) {
				if (e.get("startDate") != null && e.get("endDate") != null) {
					if (td.compareTo((LocalDate) e.get("startDate")) >= 0
					        && td.compareTo((LocalDate) e.get("endDate")) <= 0) {
						return e.get("openTimeNt") != null && e.get("closeTimeNt") != null;
					}
				} else if (e.get("startDate") != null && e.get("endDate") == null) {
					if (td.compareTo((LocalDate) e.get("startDate")) >= 0) {
						return e.get("openTimeNt") != null && e.get("closeTimeNt") != null;
					}
				}
			}

			List<Pair<LocalDate, LocalDate>> allTds = new ArrayList<>();
			for (Map<String, Object> e : tthList) {
				allTds.add(Pair.of((LocalDate) e.get("startDate"), (LocalDate) e.get("endDate")));
			}
			throw new IllegalArgumentException(String
			        .format("can NOT find tradingTimeHistInfo for %s (@ %s), all hitInfos: %s", codeOrWc, td, allTds));
		}
	}

	/**
	 * <pre>
	 * 指定的 品种 theTd (交易日) 是否有 夜盘: 交易日 T (夜盘所属自然日 为 T-1 交易日):
	 * 1. 指定品种已开通夜盘;
	 * 2. 不是公共假日前最后一个交易日.
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param theTd theTd
	 * @return boolean
	 */
	public static boolean isCnFuTdNightMarketAvailable(String codeOrWc, LocalDate theTd) {

		if (!isCnFuturesCodeHasNightMarket(codeOrWc, theTd)) {
			return false;
		}

		String cnFuName = getCnFuturesCodeName(codeOrWc);
		String exch = (String) Constants.CN_FUTURES_CODES.get(cnFuName, "exchName");
		// T-1 日, 即 夜盘所属自然日:
		String tdM1Str = tradingDaysOffset(theTd, -1).format(Constants.DATE_FORMAT_STD);
		if (Arrays.asList("SHFE", "INE").contains(exch)) {
			return Collections.binarySearch(_NO_NIGHT_MARKET_SHFE, tdM1Str) < 0;
		} else if (exch.equals("DCE")) {
			return Collections.binarySearch(_NO_NIGHT_MARKET_DCE, tdM1Str) < 0;
		} else if (exch.equals("CZCE")) {
			return Collections.binarySearch(_NO_NIGHT_MARKET_CZCE, tdM1Str) < 0;
		} else {
			throw new IllegalArgumentException(String.format("Unknown codeOrWc: %s, theTd: %s", codeOrWc, theTd));
		}
	}

	/**
	 * 
	 * @return LocalDateTime.now(Clock.systemUTC());
	 */
	public static LocalDateTime utcNowLocal() {

		return LocalDateTime.now(Clock.systemUTC());
	}

	/**
	 * ZoneId: "UTC"
	 * 
	 * @return ZonedDateTime
	 */
	public static ZonedDateTime utcNow() {

		return utcNow((ZoneId) null);
	}

	/**
	 * 缺省时区采用: "Asia/Shanghai"
	 * 
	 * @param tz tz
	 * @return ZonedDateTime
	 */
	public static ZonedDateTime utcNow(ZoneId tz) {

		ZonedDateTime zdm = ZonedDateTime.now(ZoneId.of("UTC"));
		if (tz == null) {
			return zdm;
		} else {
			return zdm.withZoneSameInstant(tz);
		}
	}

	/**
	 * 缺省 MarketSession 采用: MarketSession.SSE
	 * 
	 * @param ms MarketSession
	 * @return ZonedDateTime
	 */
	public static ZonedDateTime utcNow(MarketSession ms) {
	
		return ms == null ? utcNow(MarketSession.SSE.getZoneId()) : utcNow(ms.getZoneId());
	}

	/**
	 * 将 dateTime 的时区视为 "Asia/Shanghai":
	 * 
	 * @param dateTime dateTime
	 * @return ZonedDateTime
	 */
	public static ZonedDateTime toMarketDatetime(LocalDateTime dateTime) {

		return ZonedDateTime.of(dateTime, MarketSession.SSE.getZoneId());
	}

	/**
	 * 将 dateTime 的时区调整为 "Asia/Shanghai":
	 * 
	 * @param dateTime dateTime
	 * @return ZonedDateTime
	 */
	public static ZonedDateTime toMarketDatetime(ZonedDateTime dateTime) {

		return dateTime.withZoneSameInstant(MarketSession.SSE.getZoneId());
	}

	/**
	 * <pre>
	 * 历史行情 相应的周期 文件夹 名称:
	 * e.g.: Bar.Frequency.MINUTE -> "min_1"
	 * </pre>
	 * 
	 * @return hisdata dir
	 */
	public static String barFreqToHisDataDir(Bar.Frequency freqVal) {

		switch (freqVal) {
			case TICK:
				return "tick";
			case MINUTE:
			case MINUTE_1:
				return "min_1";
			case MINUTE_3:
				return "min_3";
			case MINUTE_5:
				return "min_5";
			case MINUTE_15:
				return "min_15";
			case MINUTE_30:
				return "min_30";
			case HOUR:
				return "min_60";
			case DAY:
				return "day";
			case WEEK:
				return "week";
			case MONTH:
				return "month";
			default:
				throw new IllegalArgumentException(String.format("Unsupported hisdata freq: %s", freqVal));
		}
	}

	/**
	 * <pre>
	 * see constants.CN_FUTURES_CODES:
	 * fields: (
	 *     "windCode", "contractName", "exchName",
	 *     "openTimeNtCa",
	 *     "openTimeNt", "closeTimeNt",
	 *     "openTimeAmCa",
	 *     "openTimeAm",
	 *     "pauseTimeAmBegin", "pauseTimeAmEnd",
	 *     "closeTimeAm",
	 *     "openTimePm", "closeTimePm",
	 *     "tradeUnit", "marginRate"
	 * )
	 * </pre>
	 * 
	 * @param wc wc
	 * @return Map&lt;String, Object&gt;
	 */
	public static Map<String, Object> getTradeInfos(String wc) {

		return getTradeInfos(wc, null);
	}

	/**
	 * <pre>
	 * see constants.CN_FUTURES_CODES:
	 * fields: (
	 *     "windCode", "contractName", "exchName",
	 *     "startDate", "endDate", // optional
	 *     "openTimeNtCa",
	 *     "openTimeNt", "closeTimeNt",
	 *     "openTimeAmCa",
	 *     "openTimeAm",
	 *     "pauseTimeAmBegin", "pauseTimeAmEnd",
	 *     "closeTimeAm",
	 *     "openTimePm", "closeTimePm",
	 *     "tradeUnit", "marginRate"
	 * )
	 * </pre>
	 * 
	 * @param wc wc
	 * @param td default: null
	 * @return Map&lt;String, Object&gt;
	 */
	public static Map<String, Object> getTradeInfos(String wc, LocalDate td) {

		String exchId = Constants.exchSffxToName(windCodeTuple(wc).getRight());
		if (Arrays.asList("SSE", "SZSE").contains(exchId)) {
			// 沪深交易所 A 股 / 指数: (TODO: 支持集合竞价, 其他类型品种 (基金, 债券, 债券回购, ...)):
			double tradeUnit;
			double marginRate;
			if (isSseSzseIndex(wc)) {
				// Double:
				tradeUnit = (double) Constants.SSE_SZSE_STOCK_MKT_I.get(wc, "tradeUnit");
				marginRate = (double) Constants.SSE_SZSE_STOCK_MKT_I.get(wc, "marginRate");
			} else {
				tradeUnit = Constants.TRADE_UNIT_SSE_SZSE_DEFAULT;
				marginRate = 1.0;
			}

			// @formatter:off
			return MapUtils.putAll(new ListOrderedMap<>(), new Object[][] {
				{"windCode", wc}, {"contractName", wc}, {"exchName", exchId},
				{"startDate", td},
				{"endDate", td},
				{"openTimeNtCa", null},
				{"openTimeNt", null}, {"closeTimeNt", null},
				{"openTimeAmCa", Constants.OPEN_TIME_AM_CALL_AUCTION},
				{"openTimeAm", Constants.OPEN_TIME_AM},
				{"pauseTimeAmBegin", null}, {"pauseTimeAmEnd", null},
				{"closeTimeAm", Constants.CLOSE_TIME_AM},
				{"openTimePm", Constants.OPEN_TIME_PM}, {"closeTimePm", Constants.CLOSE_TIME_PM},
				{"tradeUnit", tradeUnit}, {"marginRate", marginRate}
			});
			// @formatter:on
		}

		String cnFuName = getCnFuturesCodeName(wc);
		if (td == null) {
			// return constants.CN_FUTURES_CODES[cnFuName]
			return Constants.CN_FUTURES_CODES.row(cnFuName);
		} else {
			if (!Constants.CN_FUTURES_TRADING_TIME.rowKeySet().contains(cnFuName)) {
				logger.warn("Unknown cnFuture name: {}, return DEFAULT tradeInfo !", cnFuName);
				// @formatter:off
				return MapUtils.putAll(new ListOrderedMap<>(), new Object[][] {
					{"windCode", wc}, {"contractName", cnFuName}, {"exchName", exchId},
					{"startDate", td},
					{"endDate", td},
					{"openTimeNtCa", null},
					{"openTimeNt", null}, {"closeTimeNt", null},
					{"openTimeAmCa", Constants.OPEN_TIME_AM_CALL_AUCTION},
					{"openTimeAm", Constants.OPEN_TIME_AM},
					{"pauseTimeAmBegin", null}, {"pauseTimeAmEnd", null},
					{"closeTimeAm", Constants.CLOSE_TIME_AM},
					{"openTimePm", Constants.OPEN_TIME_PM}, {"closeTimePm", Constants.CLOSE_TIME_PM},
					{"tradeUnit", null}, {"marginRate", null}
				});
				// @formatter:on
			}

			@SuppressWarnings("unchecked")
			List<Map<String, Object>> tthList = (List<Map<String, Object>>) Constants.CN_FUTURES_TRADING_TIME
			        .get(cnFuName, "tradingTimeHist");
			for (Map<String, Object> e : tthList) {
				LocalDate startDate = null;
				LocalDate endDate = null;

				if (e.get("startDate") != null && e.get("endDate") != null) {

					startDate = (LocalDate) e.get("startDate");
					endDate = (LocalDate) e.get("endDate");
					if (td.compareTo(startDate) >= 0 && td.compareTo(endDate) <= 0) {
						return e;
					}
				} else if (e.get("startDate") != null && e.get("endDate") == null) {
					startDate = (LocalDate) e.get("startDate");
					if (td.compareTo(startDate) >= 0) {
						return e;
					}
				}
			}

			List<Pair<?, ?>> allTds = new ArrayList<>(tthList.size());
			for (Map<String, Object> e : tthList) {
				allTds.add(Pair.of(e.get("startDate"), e.get("endDate")));
			}
			throw new IllegalArgumentException(
			        String.format("can NOT find tradingTimeHistInfo for %s (@ %s), all hitInfos: %s", wc, td, allTds));
		}
	}

	/**
	 * 
	 * @param codeOrWc codeOrWc
	 * @return double
	 */
	public static double getCnFuTradeUnit(String codeOrWc) {

		return getCnFuTradeUnit(codeOrWc, null);
	}

	/**
	 * 
	 * @param codeOrWc codeOrWc
	 * @param td td
	 * @return double
	 */
	public static double getCnFuTradeUnit(String codeOrWc, LocalDate td) {

		Map<String, Object> cftInfos = getTradeInfos(toWindCode(codeOrWc), td);
		return (double) cftInfos.get("tradeUnit");
	}

	/**
	 * 获取指定品种 收盘 (交易暂停) last tick 最大延迟时间 (秒):
	 * 
	 * @param codeOrWc codeOrWc
	 * @return int
	 */
	public static int getCnFuLastTickDelay(String codeOrWc) {

		String cnFuName = getCnFuturesCodeName(codeOrWc);
		String exchName = (String) Constants.CN_FUTURES_CODES.get(cnFuName, "exchName");
		return Constants.LAST_TICK_DELAY_INFOS.get(exchName);
	}

	/**
	 * <pre>
	 * 依据特定 品种 的交易时段, 判断 tick 的时间是否非法: 若 td 为 None, 则 dateTime 类型需 datetime.datetime:
	 * 1. closeTimeDelaySec: 连续竞价 暂停交易开始前 / 收盘前 最后一个 Tick 的时间可能会稍晚于交易停止时间点, 根据其时间延迟是否超过 closeTimeDelaySec, 判定是否非法;
	 * 2. 集合竞价 Tick 判定: [openTimeCa, openTimeCa + closeTimeDelaySec);
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySecP default: (int) Constants.LAST_TICK_DELAY_SEC_DEFAULT
	 * @param tdP default: null
	 * @param cnFuTdInfosP default: null
	 * @param checkTdP default: false
	 * @return boolean
	 */
	public static boolean isIllegalCnFuTickDatetime(String codeOrWc, LocalDateTime dateTime,
			OptionalInt closeTimeDelaySecP, Optional<LocalDate> tdP, Optional<Map<String, Object>> cnFuTdInfosP,
	        Optional<Boolean> checkTdP) {

		int closeTimeDelaySec = closeTimeDelaySecP.orElse((int) Constants.LAST_TICK_DELAY_SEC_DEFAULT);
		LocalDate td = tdP.orElse(null);
		Map<String, Object> cnFuTdInfos = cnFuTdInfosP.orElse(null);
		boolean checkTd = checkTdP.orElse(false);

		return isIllegalCnFuTickDatetime(codeOrWc, dateTime, closeTimeDelaySec, td, cnFuTdInfos, checkTd);
	}

	/**
	 * <pre>
	 * 依据特定 品种 的交易时段, 判断 tick 的时间是否非法: 若 td 为 None, 则 dateTime 类型需 datetime.datetime:
	 * 1. closeTimeDelaySec: 连续竞价 暂停交易开始前 / 收盘前 最后一个 Tick 的时间可能会稍晚于交易停止时间点, 根据其时间延迟是否超过 closeTimeDelaySec, 判定是否非法;
	 * 2. 集合竞价 Tick 判定: [openTimeCa, openTimeCa + closeTimeDelaySec);
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySec closeTimeDelaySec
	 * @param td                dateTime 所属 交易日;
	 * @param cnFuTdInfos cnFuTdInfos
	 * @param checkTd           是否检查 dateTime 和 td 的关系;
	 * @return boolean
	 */
	public static boolean isIllegalCnFuTickDatetime(String codeOrWc, LocalDateTime dateTime, int closeTimeDelaySec,
	        LocalDate td, Map<String, Object> cnFuTdInfos, boolean checkTd) {

		return (boolean) checkIllegalCnFuTickDatetime(codeOrWc, dateTime, closeTimeDelaySec, td, cnFuTdInfos, checkTd)
		        .get("illegal");
	}

	/**
	 * <pre>
	 * 依据特定 品种 的交易时段, 判断 tick 的时间是否非法: 若 td 为 None, 则 dateTime 类型需 datetime.datetime:
	 * 1. closeTimeDelaySec: 连续竞价 暂停交易开始前 / 收盘前 最后一个 Tick 的时间可能会稍晚于交易停止时间点, 根据其时间延迟是否超过 closeTimeDelaySec, 判定是否非法;
	 * 2. 集合竞价 Tick 判定: [openTimeCa, openTimeCa + closeTimeDelaySec);
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySecP default: (int) Constants.LAST_TICK_DELAY_SEC_DEFAULT
	 * @param tdP default: null
	 * @param cnFuTdInfosP default: null
	 * @param checkTdP default: false
	 * @return Map&lt;String, Object&gt;
	 */
	public static Map<String, Object> checkIllegalCnFuTickDatetime(String codeOrWc, LocalDateTime dateTime,
	        OptionalInt closeTimeDelaySecP, Optional<LocalDate> tdP, Optional<Map<String, Object>> cnFuTdInfosP,
	        Optional<Boolean> checkTdP) {

		int closeTimeDelaySec = closeTimeDelaySecP.orElse((int) Constants.LAST_TICK_DELAY_SEC_DEFAULT);
		LocalDate td = tdP.orElse(null);
		Map<String, Object> cnFuTdInfos = cnFuTdInfosP.orElse(null);
		boolean checkTd = checkTdP.orElse(false);

		return checkIllegalCnFuTickDatetime(codeOrWc, dateTime, closeTimeDelaySec, td, cnFuTdInfos, checkTd);
	}

	/**
	 * <pre>
	 * 依据特定 品种 的交易时段, 判断 tick 的时间是否非法: 若 td 为 None, 则 dateTime 类型需 datetime.datetime:
	 * 1. closeTimeDelaySec: 连续竞价 暂停交易开始前 / 收盘前 最后一个 Tick 的时间可能会稍晚于交易停止时间点, 根据其时间延迟是否超过 closeTimeDelaySec, 判定是否非法;
	 * 2. 集合竞价 Tick 判定: [openTimeCa, openTimeCa + closeTimeDelaySec);
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySec closeTimeDelaySec
	 * @param td                dateTime 所属 交易日;
	 * @param cnFuTdInfos cnFuTdInfos
	 * @param checkTd           是否检查 dateTime 和 td 的关系;
	 * @return dict: <"illegal", "isTday", "td", "cnFuTdInfos", "nightMktCanceled">.
	 */
	public static Map<String, Object> checkIllegalCnFuTickDatetime(String codeOrWc, LocalDateTime dateTime,
	        int closeTimeDelaySec, LocalDate td, Map<String, Object> cnFuTdInfos, boolean checkTd) {

		// @formatter:off
		Map<String, Object> retDict = MapUtils.putAll(new HashMap<>(), new Object[][] {
				{ "illegal", false },
				{ "isTday", false },
				{ "td", null },
				{ "cnFuTdInfos", null },
				{ "nightMktCanceled", false }
			});
		// @formatter:on

		LocalTime tm = dateTime.toLocalTime();
		boolean isTday = isTradingDay(dateTime.toLocalDate());

		// 检查 td 与 dateTime 的关系, 或当 td 为 None 时自动判断:
		boolean isNightMtk = tm.compareTo(Constants.NIGHT_MKT_TIME_CN_FU) > 0
				|| tm.compareTo(Constants.AM_MKT_TIME_CN_FU) < 0 && !isTday;
		if (checkTd && td != null) {
			if (isNightMtk) {
				if (!td.equals(tradingDaysOffset(dateTime.toLocalDate(), 1))) {
					throw new IllegalArgumentException(
					        String.format("barOpenDm: %s is NOT belongs to td: %s !", dateTime, td));
				}
			} else {
				if (!td.equals(dateTime.toLocalDate())) {
					throw new IllegalArgumentException(
					        String.format("barOpenDm: %s is NOT belongs to td: %s !", dateTime, td));
				}
			}
		}

		if (td == null) {
			if (isNightMtk) {
				// 夜盘时段 (包括 午夜盘):
				td = tradingDaysOffset(dateTime.toLocalDate(), 1);
			} else {
				td = dateTime.toLocalDate();
			}
		}

		Map<String, Object> cftInfos = (cnFuTdInfos == null) ? getTradeInfos(codeOrWc, td) : cnFuTdInfos;
		LocalTime openTimeNtCa = (LocalTime) cftInfos.get("openTimeNtCa");
		LocalTime openTimeNt = (LocalTime) cftInfos.get("openTimeNt");
		LocalTime closeTimeNt = (LocalTime) cftInfos.get("closeTimeNt");
		LocalTime openTimeAmCa = (LocalTime) cftInfos.get("openTimeAmCa");
		LocalTime openTimeAm = (LocalTime) cftInfos.get("openTimeAm");
		LocalTime pauseTimeAmBegin = (LocalTime) cftInfos.get("pauseTimeAmBegin");
		LocalTime pauseTimeAmEnd = (LocalTime) cftInfos.get("pauseTimeAmEnd");
		LocalTime closeTimeAm = (LocalTime) cftInfos.get("closeTimeAm");
		LocalTime openTimePm = (LocalTime) cftInfos.get("openTimePm");
		LocalTime closeTimePm = (LocalTime) cftInfos.get("closeTimePm");

		boolean nightMktOk = (openTimeNt != null & closeTimeNt != null);
		// 针对公共节假日后首个交易日无夜盘的情况:
		boolean nightMktCanceled = false;

		String cnFuName = getCnFuturesCodeName(codeOrWc);
		String exchId = (String) Constants.CN_FUTURES_CODES.get(cnFuName, "exchName");
		if (nightMktOk) {
			switch (exchId) {
				case "SHFE":
					if (_NO_NIGHT_MARKET_SHFE.contains(td.format(Constants.DATE_FORMAT_STD))) {
						nightMktCanceled = true;
					}
					break;
				case "DCE":
					if (_NO_NIGHT_MARKET_DCE.contains(td.format(Constants.DATE_FORMAT_STD))) {
						nightMktCanceled = true;
					}
					break;
				case "CZCE":
					if (_NO_NIGHT_MARKET_CZCE.contains(td.format(Constants.DATE_FORMAT_STD))) {
						nightMktCanceled = true;
					}
					break;
			}
		}

		retDict.put("isTday", isTday);
		retDict.put("td", td);
		retDict.put("cnFuTdInfos", cftInfos);
		retDict.put("nightMktCanceled", nightMktCanceled);

		// 不是非交易日午夜盘的情况, 认为 tick 的 datetime 非法:
		if (!isTday && !nightMktOk) {
			retDict.put("illegal", true);
			return retDict;
		}

		/*
		 * 连续竞价:
		 */
		// 上午盘暂停时段:
		if (pauseTimeAmBegin != null && pauseTimeAmEnd != null) {
			if (tm.compareTo(pauseTimeAmBegin.plusSeconds(closeTimeDelaySec)) >= 0
			        && tm.compareTo(pauseTimeAmEnd) < 0) {
				retDict.put("illegal", true);
				return retDict;
			}
		}
		// 上午盘, 下午盘:
		if (tm.compareTo(openTimeAm) >= 0 && tm.compareTo(closeTimeAm.plusSeconds(closeTimeDelaySec)) < 0
		        || tm.compareTo(openTimePm) >= 0 && tm.compareTo(closeTimePm.plusSeconds(closeTimeDelaySec)) < 0) {
			// 上午盘, 下午盘连续竞价 (不含上午盘暂停时段):
			retDict.put("illegal", false);
			return retDict;
		}
		// 夜盘:
		if (nightMktOk && !nightMktCanceled) {
			if (closeTimeNt.compareTo(openTimeNt) > 0) {
				// 次日 00:00:00 前收盘:
				if (tm.compareTo(openTimeNt) >= 0 && tm.compareTo(closeTimeNt.plusSeconds(closeTimeDelaySec)) < 0) {
					retDict.put("illegal", false);
					return retDict;
				}
			} else {
				// 次日 00:00:00 后收盘:
				if (tm.compareTo(openTimeNt) >= 0 || tm.compareTo(closeTimeNt.plusSeconds(closeTimeDelaySec)) < 0) {
					retDict.put("illegal", false);
					return retDict;
				}
			}
		}
		/*
		 * 集合竞价:
		 */
		// 夜盘集合竞价推迟到早盘集合竞价:
		if (openTimeNtCa != null) {
			// 公共假日后第一个交易日 (T 日) 无夜盘
			if (nightMktCanceled) {
				if (closeTimeDelaySec > 0) {
					if (tm.compareTo(Constants.OPEN_TIME_CN_FU_AM_CALL_AUCTION) >= 0 && tm
					        .compareTo(Constants.OPEN_TIME_CN_FU_AM_CALL_AUCTION.plusSeconds(closeTimeDelaySec)) < 0) {
						retDict.put("illegal", false);
						return retDict;
					}
				} else {
					if (tm.equals(Constants.OPEN_TIME_CN_FU_AM_CALL_AUCTION)) {
						retDict.put("illegal", false);
						return retDict;
					}
				}
			} else {
				if (closeTimeDelaySec > 0) {
					if (tm.compareTo(openTimeNtCa) >= 0
					        && tm.compareTo(openTimeNtCa.plusSeconds(closeTimeDelaySec)) < 0) {
						retDict.put("illegal", false);
						return retDict;
					}
				} else {
					if (tm.equals(openTimeNtCa)) {
						retDict.put("illegal", false);
						return retDict;
					}
				}
			}
		}
		// 上午盘集合竞价:
		if (openTimeAmCa != null) {
			if (closeTimeDelaySec > 0) {
				if (tm.compareTo(openTimeAmCa) >= 0 && tm.compareTo(openTimeAmCa.plusSeconds(closeTimeDelaySec)) < 0) {
					retDict.put("illegal", false);
					return retDict;
				}
			} else {
				if (tm.equals(openTimeAmCa)) {
					retDict.put("illegal", false);
					return retDict;
				}
			}
		}
		// 其他情况, 认为 tick 的 datetime 非法:
		retDict.put("illegal", true);

		return retDict;
	}

	/**
	 * <p>
	 * 是否为 连续竞价 时段 首个 Bar: 返回一个 tuple (True / False, 集合竞价开盘时间)
	 * </p>
	 * <p>
	 * 1. 判定条件: startTm == openTime
	 * </p>
	 * <p>
	 * 夜盘连续竞价第一个 TimeRange, 上午盘连续竞价第一个 TimeRange 均认为 True (简化 公共假日 后首个交易日无夜盘, 集合竞价顺延至 上午盘 的情况):
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param tdP default: null
	 * @param cnFuTdInfosP default: null
	 * @param checkTdP default: false
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isCnFuContia1stTimeRange(String codeOrWc, LocalDateTime startDm,
	        LocalDateTime endDm, Optional<LocalDate> tdP, Optional<Map<String, Object>> cnFuTdInfosP,
	        Optional<Boolean> checkTdP) {

		LocalDate td = tdP.orElse(null);
		Map<String, Object> cnFuTdInfos = cnFuTdInfosP.orElse(null);
		boolean checkTd = checkTdP.orElse(false);

		return isCnFuContia1stTimeRange(codeOrWc, startDm, endDm, td, cnFuTdInfos, checkTd);
	}

	/**
	 * <p>
	 * 是否为 连续竞价 时段 首个 Bar: 返回一个 tuple (True / False, 集合竞价开盘时间)
	 * </p>
	 * <p>
	 * 1. 判定条件: startTm == openTime
	 * </p>
	 * <p>
	 * 夜盘连续竞价第一个 TimeRange, 上午盘连续竞价第一个 TimeRange 均认为 True (简化 公共假日 后首个交易日无夜盘, 集合竞价顺延至 上午盘 的情况):
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param td td
	 * @param cnFuTdInfos cnFuTdInfos
	 * @param checkTd checkTd
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isCnFuContia1stTimeRange(String codeOrWc, LocalDateTime startDm,
	        LocalDateTime endDm, LocalDate td, Map<String, Object> cnFuTdInfos, boolean checkTd) {

		LocalTime startTm = startDm.toLocalTime();
		Map<String, Object> cftInfos = (cnFuTdInfos == null) ? getTradeInfos(toWindCode(codeOrWc), td) : cnFuTdInfos;

		LocalTime openTimeNtCa = (LocalTime) cftInfos.get("openTimeNtCa");
		LocalTime openTimeNt = (LocalTime) cftInfos.get("openTimeNt");
		LocalTime closeTimeNt = (LocalTime) cftInfos.get("closeTimeNt");
		LocalTime openTimeAmCa = (LocalTime) cftInfos.get("openTimeAmCa");
		LocalTime openTimeAm = (LocalTime) cftInfos.get("openTimeAm");

		if (startTm.compareTo(Constants.NIGHT_MKT_TIME_CN_FU) > 0) {
			// startDm 非法?
			if (openTimeNt == null && closeTimeNt == null) {
				return Pair.of(false, null);
			}

			if (startTm.equals(openTimeNt)) {
				if (openTimeNtCa != null) {
					return Pair.of(true, LocalDateTime.of(startDm.toLocalDate(), openTimeNtCa));
				}
			}
		} else {
			if (startTm.equals(openTimeAm)) {
				if (openTimeAmCa != null) {
					if (openTimeNt == null && closeTimeNt == null) {
						// 中国期货 夜盘: 简化 公共假日 后首个交易日无夜盘, 集合竞价顺延至上午盘的情况:
						return Pair.of(true,
						        LocalDateTime.of(startDm.toLocalDate(), Constants.OPEN_TIME_CN_FU_AM_CALL_AUCTION));
					} else {
						// 无夜盘品种:
						return Pair.of(true, LocalDateTime.of(startDm.toLocalDate(), openTimeAmCa));
					}
				}
			}
		}

		return Pair.of(false, null);
	}

	/**
	 * <p>
	 * 是否为 连续竞价 时段 收盘 (包括 暂停交易) 最后一个 Bar: 返回一个 tuple (True / False, 收盘时间 + closeTimeDelaySec)
	 * </p>
	 * <p>
	 * 1. 判断条件: endTm = closeTime
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param closeTimeDelaySecP default: -1
	 * @param tdP default: null
	 * @param cnFuTdInfosP default: null
	 * @param checkTdP default: false
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isCnFuContiaLastTimeRange(String codeOrWc, LocalDateTime startDm,
	        LocalDateTime endDm, OptionalInt closeTimeDelaySecP, Optional<LocalDate> tdP,
	        Optional<Map<String, Object>> cnFuTdInfosP, Optional<Boolean> checkTdP) {

		int closeTimeDelaySec = closeTimeDelaySecP.orElse(-1);
		LocalDate td = tdP.orElse(null);
		Map<String, Object> cnFuTdInfos = cnFuTdInfosP.orElse(null);
		boolean checkTd = checkTdP.orElse(false);

		return isCnFuContiaLastTimeRange(codeOrWc, startDm, endDm, closeTimeDelaySec, td, cnFuTdInfos, checkTd);
	}

	/**
	 * <p>
	 * 是否为 连续竞价 时段 收盘 (包括 暂停交易) 最后一个 Bar: 返回一个 tuple (True / False, 收盘时间 + closeTimeDelaySec)
	 * </p>
	 * <p>
	 * 1. 判断条件: endTm = closeTime
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param closeTimeDelaySec closeTimeDelaySec
	 * @param td td
	 * @param cnFuTdInfos cnFuTdInfos
	 * @param checkTd checkTd
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isCnFuContiaLastTimeRange(String codeOrWc, LocalDateTime startDm,
	        LocalDateTime endDm, int closeTimeDelaySec, LocalDate td, Map<String, Object> cnFuTdInfos,
	        boolean checkTd) {

		LocalTime startTm = startDm.toLocalTime();
		LocalTime endTm = endDm.toLocalTime();
		Map<String, Object> cftInfos = (cnFuTdInfos == null) ? getTradeInfos(toWindCode(codeOrWc), td) : cnFuTdInfos;

		LocalTime openTimeAm = (LocalTime) cftInfos.get("openTimeAm");
		LocalTime openTimeNt = (LocalTime) cftInfos.get("openTimeNt");
		LocalTime closeTimeNt = (LocalTime) cftInfos.get("closeTimeNt");
		LocalTime pauseTimeAmBegin = (LocalTime) cftInfos.get("pauseTimeAmBegin");
		LocalTime closeTimeAm = (LocalTime) cftInfos.get("closeTimeAm");
		LocalTime closeTimePm = (LocalTime) cftInfos.get("closeTimePm");

		if (closeTimeDelaySec < 0) {
			closeTimeDelaySec = getCnFuLastTickDelay(codeOrWc);
		}

		if (startTm.compareTo(Constants.NIGHT_MKT_TIME_CN_FU) > 0) {
			// startDm 非法?
			if (openTimeNt == null && closeTimeNt == null) {
				return Pair.of(false, null);
			}
			// 夜盘收盘早于 00:00:00, 最后一个 Bar:
			if (endTm.equals(closeTimeNt)) {
				return Pair.of(true, endDm.plusSeconds(closeTimeDelaySec));
			}
		} else {
			if (openTimeNt != null && closeTimeNt != null && closeTimeNt.compareTo(openTimeAm) < 0) {
				if (endTm.equals(closeTimeNt)) {
					// 夜盘收盘晚于 00:00:00, 最后一个 Bar:
					return Pair.of(true, endDm.plusSeconds(closeTimeDelaySec));
				}
			}
			// 上午盘暂停时间开始前最后一个 Bar:
			if (endTm.equals(pauseTimeAmBegin)) {
				return Pair.of(true, endDm.plusSeconds(closeTimeDelaySec));
			}
			// 上午收盘前最后一个 Bar:
			if (endTm.equals(closeTimeAm) || endTm.equals(closeTimePm)) {
				return Pair.of(true, endDm.plusSeconds(closeTimeDelaySec));
			}
		}

		return Pair.of(false, null);
	}

	/**
	 *
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySecP default: (int) Constants.LAST_TICK_DELAY_SEC_DEFAULT
	 * @param tdP default: dateTime.toLocalDate()
	 * @param checkTdP default: false
	 * @return isSseSzseTickDatetimeInBarTimeRange
	 */
	public static boolean isSseSzseTickDatetimeInBarTimeRange(String codeOrWc, LocalDateTime dateTime, OptionalInt closeTimeDelaySecP,
	                                                          Optional<LocalDate> tdP, Optional<Boolean> checkTdP) {

		int closeTimeDelaySec = closeTimeDelaySecP.orElse((int) Constants.LAST_TICK_DELAY_SEC_DEFAULT);
		LocalDate td = dateTime.toLocalDate();
		boolean checkTd = checkTdP.orElse(false);

		return isSseSzseTickDatetimeInBarTimeRange(codeOrWc, dateTime, closeTimeDelaySec, td, checkTd);
	}

	/**
	 *
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySec closeTimeDelaySec
	 * @param td td
	 * @param checkTd checkTd
	 * @return isSseSzseTickDatetimeInBarTimeRange
	 */
	public static boolean isSseSzseTickDatetimeInBarTimeRange(String codeOrWc, LocalDateTime dateTime, int closeTimeDelaySec,
	                                                          LocalDate td, boolean checkTd) {

		LocalTime tm = dateTime.toLocalTime();
		return tm.compareTo(Constants.OPEN_TIME_AM_CALL_AUCTION) >= 0 && tm.compareTo(Constants.OPEN_TIME_AM_CALL_AUCTION.plusSeconds(closeTimeDelaySec)) < 0
				|| tm.compareTo(Constants.OPEN_TIME_AM) >= 0 && tm.compareTo(Constants.CLOSE_TIME_AM.plusSeconds(closeTimeDelaySec)) < 0
				|| tm.compareTo(Constants.OPEN_TIME_PM) >= 0 && tm.compareTo(Constants.CLOSE_TIME_PM.plusSeconds(closeTimeDelaySec)) < 0;
	}

	/**
	 * <pre>
	 * 判断 沪深交易所 (A 股, 场内基金, 股票指数, 暂不支持债券回购等) 标的 Tick 时间是否非法:
	 * 09:25:00, 09:30 ~ 11:30, 13:00 ~ 15:00 (14:57 ~ 14:59 之间的集合竞价 Tick 视为非法)
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySecP default: (int) Constants.LAST_TICK_DELAY_SEC_DEFAULT
	 * @param tdP default: dateTime.toLocalDate()
	 * @param checkTdP default: false
	 * @return isIllegalSseSzseTickDatetime
	 */
	public static boolean isIllegalSseSzseTickDatetime(String codeOrWc, LocalDateTime dateTime, OptionalInt closeTimeDelaySecP,
	        Optional<LocalDate> tdP, Optional<Boolean> checkTdP) {

		int closeTimeDelaySec = closeTimeDelaySecP.orElse((int) Constants.LAST_TICK_DELAY_SEC_DEFAULT);
		LocalDate td = dateTime.toLocalDate();
		boolean checkTd = checkTdP.orElse(false);

		return isIllegalSseSzseTickDatetime(codeOrWc, dateTime, closeTimeDelaySec, td, checkTd);
	}

	/**
	 * <pre>
	 * 判断 沪深交易所 (A 股, 场内基金, 股票指数, 暂不支持债券回购等) 标的 Tick 时间是否非法:
	 * 09:25:00, 09:30 ~ 11:30, 13:00 ~ 15:00 (14:57 ~ 14:59 之间的集合竞价 Tick 视为非法)
	 * </pre>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param dateTime dateTime
	 * @param closeTimeDelaySec closeTimeDelaySec
	 * @param td td
	 * @param checkTd checkTd
	 * @return isIllegalSseSzseTickDatetime
	 */
	public static boolean isIllegalSseSzseTickDatetime(String codeOrWc, LocalDateTime dateTime, int closeTimeDelaySec,
	        LocalDate td, boolean checkTd) {

		LocalTime tm = dateTime.toLocalTime();
		// 沪深交易所代码前 2 位:
		String cd2 = codeOrWc.substring(0, 2);
		// 交易所类型 ("SSE", "SZSE", "SSE_SZSE"): 单一品种为单一市场, 沪深指数可以是跨市场:
		SSE_SZSE_INDEX_MARKET exchCls = null;
		if (Constants.SSE_SZSE_STOCK_MKT_I.rowKeySet().contains(codeOrWc)) {
			exchCls = (SSE_SZSE_INDEX_MARKET) Constants.SSE_SZSE_STOCK_MKT_I.get(codeOrWc, "market");
		} else {
			if (Arrays.asList("00", "30", "15", "16", "18", "12", "39").contains(cd2)) {
				// 深: A 股, 基金, 可转债, 深证系列指数:
				exchCls = SSE_SZSE_INDEX_MARKET.SZSE;
			} else if (Arrays.asList("60", "68", "50", "58", "51", "11").contains(cd2)) {
				// 沪: A 股, 基金, 可转债:
				exchCls = SSE_SZSE_INDEX_MARKET.SSE;
			} else {
				throw new IllegalArgumentException(String.format("Unknown codeOrWc: %s !", codeOrWc));
			}
		}
		// @formatter:off
		if (closeTimeDelaySec > 0) {
			if (tm.compareTo(Constants.OPEN_TIME_AM_CALL_AUCTION) >= 0
					&& tm.compareTo(Constants.OPEN_TIME_AM_CALL_AUCTION.plusSeconds(closeTimeDelaySec)) < 0
						|| tm.compareTo(Constants.OPEN_TIME_AM) >= 0
							&& tm.compareTo(Constants.CLOSE_TIME_AM.plusSeconds(closeTimeDelaySec)) < 0
						|| tm.compareTo(Constants.OPEN_TIME_PM) >= 0
							&& tm.compareTo(Constants.CLOSE_MINUTE_PM_CALL_AUCTION.plusSeconds(closeTimeDelaySec)) < 0) {
				return false;
			} else {
				// 深: A 股自 2006-07-01 起实施收盘集合竞价:
				if (exchCls == SSE_SZSE_INDEX_MARKET.SZSE) {
					if (td.compareTo(Constants.SZSE_CLOSE_CALL_AUCTION_START) < 0) {
						return tm.compareTo(Constants.CLOSE_MINUTE_PM_CALL_AUCTION) < 0
								|| tm.compareTo(Constants.CLOSE_TIME_PM.plusSeconds(closeTimeDelaySec)) >= 0;
					}
				} else {  // 沪深跨市场指数或上交所品种:
					// 沪: A 股自 2018-08-20 起实施收盘集合竞价 (基金, 债券暂未实行):
					if (td.compareTo(Constants.SSE_CLOSE_CALL_AUCTION_START) < 0
							|| Arrays.asList("50", "51", "11").contains(cd2)) {
						return tm.compareTo(Constants.CLOSE_MINUTE_PM_CALL_AUCTION) < 0
								|| tm.compareTo(Constants.CLOSE_TIME_PM.plusSeconds(closeTimeDelaySec)) >= 0;
					}
				}
				return true;
			}
		} else {
			if (tm.compareTo(Constants.OPEN_TIME_AM_CALL_AUCTION) == 0
					|| tm.compareTo(Constants.OPEN_TIME_AM) >= 0 && tm.compareTo(Constants.CLOSE_TIME_AM) < 0
					|| tm.compareTo(Constants.OPEN_TIME_PM) >= 0 && tm.compareTo(Constants.CLOSE_MINUTE_PM_CALL_AUCTION) < 0) {
				return false;
			} else {
				// 深: A 股自 2006-07-01 起实施收盘集合竞价:
				boolean isInCa = tm.compareTo(Constants.CLOSE_MINUTE_PM_CALL_AUCTION) >= 0
						&& tm.compareTo(Constants.CLOSE_TIME_PM) < 0;
				if (exchCls == SSE_SZSE_INDEX_MARKET.SZSE) {
					if (td.compareTo(Constants.SZSE_CLOSE_CALL_AUCTION_START) < 0) {
						return !isInCa;
					}
				} else { // // 沪深跨市场指数或上交所品种:
					// 沪: A 股自 2018-08-20 起实施收盘集合竞价 (基金, 债券暂未实行):
					if (td.compareTo(Constants.SSE_CLOSE_CALL_AUCTION_START) < 0
							|| Arrays.asList("50", "51", "11").contains(cd2)) {
						return !isInCa;
					}
				}
				return true;
			}
		}
		// @formatter:on
	}

	/**
	 * <p>
	 * 判断 沪深交易所 (A 股, 场内基金, 暂不支持债券回购等) 标的 连续竞价 时段 首个 Bar:
	 * 返回一个 tuple (True / False, 集合竞价开盘时间)
	 * </p>
	 * <p>
	 * 1. 判断条件: startTm == openTime;
	 * </p>
	 * <p>
	 * 2. windCode 支持任意品种, code 不支持指数:
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param tdP default: null
	 * @param checkTdP default: false
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isSseSzseContia1stTimeRange(String codeOrWc,
			LocalDateTime startDm, LocalDateTime endDm, Optional<LocalDate> tdP, Optional<Boolean> checkTdP) {

		LocalDate td = tdP.orElse(null);
		boolean checkTd = checkTdP.orElse(false);

		return isSseSzseContia1stTimeRange(codeOrWc, startDm, endDm, td, checkTd);
	}

	/**
	 * <p>
	 * 判断 沪深交易所 (A 股, 场内基金, 暂不支持债券回购等) 标的 连续竞价 时段 首个 Bar:
	 * 返回一个 tuple (True / False, 集合竞价开盘时间)
	 * </p>
	 * <p>
	 * 1. 判断条件: startTm == openTime;
	 * </p>
	 * <p>
	 * 2. windCode 支持任意品种, code 不支持指数:
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param td td
	 * @param checkTd checkTd
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isSseSzseContia1stTimeRange(String codeOrWc,
			LocalDateTime startDm, LocalDateTime endDm, LocalDate td, boolean checkTd) {

		if (td == null) {
			td = startDm.toLocalDate();
		}
		LocalTime startTm = startDm.toLocalTime();
		if (startTm.compareTo(Constants.OPEN_TIME_AM) == 0) {
			return Pair.of(true, LocalDateTime.of(td, Constants.OPEN_TIME_AM_CALL_AUCTION));
		} else {
			return Pair.of(false, startDm);
		}
	}
	/**
	 * <p>
	 * 判断 沪深交易所 (A 股, 场内基金, 暂不支持债券回购等) 标的 连续竞价 时段 收盘 (包括 暂停交易) 最后一个 Bar:
	 * 返回一个 tuple (True / False, 收盘时间 + closeTimeDelaySec)
	 * </p>
	 * <p>
	 * 1. 判断条件: endTm = closeTime;
	 * </p>
	 * <p>
	 * 2. windCode 支持任意品种, code 不支持指数:
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param closeTimeDelaySecP default: (int) Constants.LAST_TICK_DELAY_SEC_DEFAULT
	 * @param tdP default: null
	 * @param checkTdP default: false
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isSseSzseContiaLastTimeRange(String codeOrWc,
			LocalDateTime startDm, LocalDateTime endDm, OptionalInt closeTimeDelaySecP, Optional<LocalDate> tdP, Optional<Boolean> checkTdP) {

		int closeTimeDelaySec = closeTimeDelaySecP.orElse((int) Constants.LAST_TICK_DELAY_SEC_DEFAULT);
		LocalDate td = tdP.orElse(null);
		boolean checkTd = checkTdP.orElse(false);

		return isSseSzseContiaLastTimeRange(codeOrWc, startDm, endDm, closeTimeDelaySec, td, checkTd);
	}

	/**
	 * <p>
	 * 判断 沪深交易所 (A 股, 场内基金, 暂不支持债券回购等) 标的 连续竞价 时段 收盘 (包括 暂停交易) 最后一个 Bar:
	 * 返回一个 tuple (True / False, 收盘时间 + closeTimeDelaySec)
	 * </p>
	 * <p>
	 * 1. 判断条件: endTm = closeTime;
	 * </p>
	 * <p>
	 * 2. windCode 支持任意品种, code 不支持指数:
	 * </p>
	 * 
	 * @param codeOrWc codeOrWc
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param closeTimeDelaySec closeTimeDelaySec
	 * @param td td
	 * @param checkTd checkTd
	 * @return (boolean, LocalDateTime)
	 */
	public static Pair<Boolean, LocalDateTime> isSseSzseContiaLastTimeRange(String codeOrWc,
			LocalDateTime startDm, LocalDateTime endDm, int closeTimeDelaySec, LocalDate td, boolean checkTd) {

		if (td == null) {
			td = endDm.toLocalDate();
		}

		LocalTime endTm = endDm.toLocalTime();
		// 沪深交易所代码前 2 位:
		String cd2 = codeOrWc.substring(0, 2);
		// 交易所类型 ("SSE", "SZSE", "SSE_SZSE"): 单一品种为单一市场, 沪深指数可以是跨市场:
		SSE_SZSE_INDEX_MARKET exchCls = null;

		if (Constants.SSE_SZSE_STOCK_MKT_I.rowKeySet().contains(codeOrWc)) {
			exchCls = (SSE_SZSE_INDEX_MARKET) Constants.SSE_SZSE_STOCK_MKT_I.get(codeOrWc, "market");
		} else {
			if (Arrays.asList("00", "30", "15", "16", "18", "12", "39").contains(cd2)) {
				// 深: A 股, 基金, 可转债, 深证系列指数:
				exchCls = SSE_SZSE_INDEX_MARKET.SZSE;
			} else if (Arrays.asList("60", "68", "50", "58", "51", "11").contains(cd2)) {
				// 沪: A 股, 基金, 可转债:
				exchCls = SSE_SZSE_INDEX_MARKET.SSE;
			} else {
				throw new IllegalArgumentException(String.format("Unknown codeOrWc: %s !", codeOrWc));
			}
		}
		// 连续竞价 上 / 下午收盘:
		if (Arrays.asList(Constants.CLOSE_TIME_AM, Constants.CLOSE_TIME_PM).contains(endTm)) {
			return Pair.of(true, endDm.plusSeconds(closeTimeDelaySec));
		}
		// 深: A 股自 2006-07-01 起实施收盘集合竞价:
		if (exchCls == SSE_SZSE_INDEX_MARKET.SZSE) {
			if (td.compareTo(Constants.SZSE_CLOSE_CALL_AUCTION_START) >= 0) {
				if (endTm.compareTo(Constants.CLOSE_MINUTE_PM_CALL_AUCTION) == 0) {
					return Pair.of(true, endDm.plusSeconds(closeTimeDelaySec));
				}
			}
		} else { // 沪深跨市场指数或上交所品种:
			// 沪: A 股自 2018-08-20 起实施收盘集合竞价 (基金, 债券暂未实行):
			if (td.compareTo(Constants.SSE_CLOSE_CALL_AUCTION_START) >= 0) {
				if (!Arrays.asList("50", "51", "11").contains(cd2)) {
					if (endTm.compareTo(Constants.CLOSE_MINUTE_PM_CALL_AUCTION) == 0) {
						return Pair.of(true, endDm.plusSeconds(closeTimeDelaySec));
					}
				}
			}
		}

		return Pair.of(false, endDm);
	}

	/**
	 * 可忽略 WindPy 附加的 "数据来源: Wind"
	 * 
	 * @param csvPath csvPath
	 * @param colNameP default: "wind_code"
	 * @param encP default: "GBK"
	 * @return List&lt;String&gt;
	 * @throws IOException IOException
	 */
	public static List<String> getWindcodesFrom(Path csvPath, Optional<String> colNameP, Optional<String> encP) throws IOException {

		String colName = colNameP.orElse("wind_code");
		String enc = encP.orElse("GBK");
		return getWindcodesFrom(csvPath, colName, enc);
	}

	/**
	 * 可忽略 WindPy 附加的 "数据来源: Wind"
	 * 
	 * @param csvPath csvPath
	 * @param colName colName
	 * @param enc enc
	 * @return List&lt;String&gt;
	 * @throws IOException IOException
	 */
	public static List<String> getWindcodesFrom(Path csvPath, String colName, String enc) throws IOException {

		List<String> ret = new ArrayList<>();

		try (CSVParser parser = CSVParser.parse(csvPath, Charset.forName(enc),
		        CSVFormat.DEFAULT.withFirstRecordAsHeader())) {
			List<CSVRecord> records = parser.getRecords();
			for (int i = 0; i < records.size(); i++) {
				CSVRecord rec = records.get(i);
				if (rec.isConsistent()) {
					ret.add(rec.get(colName));
				} else {
					if (i != records.size() - 1) {
						throw new IOException(String.format("Bad csv content: %s @ (file: %s)", rec, csvPath));
					}
				}
			}
		}
		return ret;
	}

	/**
	 * <pre>
	 * e.g.: filter_cn_fu_for(["JM1905.DCE", "J1905.DCE", "RB1905.SHF", "HC1905.SHF", "CU1906.SHF", ...], ["RB", "HC"])
	 *     -> ["RB1905.SHF", "HC1905.SHF"]
	 * </pre>
	 * 
	 * 不支持仿真品种
	 * 
	 * @param wcs wcs
	 * @param cnFuNames cnFuNames
	 */
	public static List<String> filterCnFuFor(List<String> wcs, List<String> cnFuNames) {

		if (cnFuNames.size() == 0) {
			return Collections.emptyList();
		}

		String wcsRe = String.join("|", cnFuNames);
		List<String> ret = new ArrayList<>();
		for (String e : wcs) {
			if (Pattern.matches(String.format("(%s)\\d+\\.\\w+", wcsRe), e)) {
				ret.add(e);
			}
		}
		return ret;
	}

	/**
	 * <pre>
	 * 获取指定目录下所有 .csv 的 code (或 windcode): cnFuNamesFilter 和 cdsOrWcsRegex 参数最多只能二选一:<br />
	 * cnFuNamesFilter: 期货品种名称代码;
	 * cdsOrWcsRegex: 代码正则表达式.
	 * </pre>
	 * 
	 * @param csvDir csvDir
	 * @param cnFuNamesFilterP default: Collections.emptyList()
	 * @param cdsOrWcsRegexP default: null
	 * @return List&lt;String&gt;
	 */
	public static List<String> getAllCodesOrWcsFromDir(Path csvDir, Optional<List<String>> cnFuNamesFilterP,
	        Optional<String> cdsOrWcsRegexP) {

		List<String> cnFuNamesFilter = cnFuNamesFilterP.orElse(Collections.emptyList());
		String cdsOrWcsRegex = cdsOrWcsRegexP.orElse(null);

		return getAllCodesOrWcsFromDir(csvDir, cnFuNamesFilter, cdsOrWcsRegex);
	}

	/**
	 * <pre>
	 * 获取指定目录下所有 .csv 的 code (或 windcode): cnFuNamesFilter 和 cdsOrWcsRegex 参数最多只能二选一:<br />
	 * cnFuNamesFilter: 期货品种名称代码;
	 * cdsOrWcsRegex: 代码正则表达式.
	 * </pre>
	 * 
	 * @param csvDir csvDir
	 * @param cnFuNamesFilter cnFuNamesFilter
	 * @param cdsOrWcsRegex cdsOrWcsRegex
	 * @return List&lt;String&gt;
	 */
	public static List<String> getAllCodesOrWcsFromDir(Path csvDir, List<String> cnFuNamesFilter,
	        String cdsOrWcsRegex) {

		if (cnFuNamesFilter.size() > 0 && cdsOrWcsRegex != null && cdsOrWcsRegex.length() > 0) {
			throw new IllegalArgumentException(
			        String.format("Alternative opt params: cnFuNamesFilter (%s), cdsOrWcsRegex (%s) !", cnFuNamesFilter,
			                cdsOrWcsRegex));
		}

		List<String> ret = new ArrayList<>();
		if (csvDir.toFile().isDirectory()) {
			for (String e : Objects.requireNonNull(csvDir.toFile().list((dir, name) -> {
				return name.endsWith(".csv");
			}))) {
				ret.add(e.replace(".csv", ""));
			}
		}

		if (cnFuNamesFilter.size() > 0) {

			return filterCnFuFor(ret, cnFuNamesFilter);

		} else if (cdsOrWcsRegex != null && cdsOrWcsRegex.length() > 0) {

			List<String> filtered = new ArrayList<>();
			Pattern ptn = Pattern.compile(cdsOrWcsRegex);
			for (String e : ret) {
				if (ptn.matcher(e).matches()) {
					filtered.add(e);
				}
			}
			return filtered;
	
		} else {
	
			return ret;
		}
	}

	/**
	 *
	 * @param wc wc
	 * @param theDate theDate
	 * @param freq freq
	 * @return Pair&lt;Boolean, Path&gt;
	 */
	public static Pair<Boolean, Path> isHisDataFileExists(String wc, LocalDate theDate, Bar.Frequency freq, HisDataSource hdSrc) {

		Path rootP = null;
		Path hisDataCsvP;
		switch (hdSrc) {
			case TUSHARE:
				rootP = TS_HIS_DATA_BASE_DIR_DEFAULT;
				break;
			case WIND:
				rootP = WIND_HIS_DATA_BASE_DIR_DEFAULT;
				break;
			case ZIPLINE:
				rootP = ZIPLINE_HIS_DATA_BASE_DIR_DEFAULT;
				break;
		}
		if (hdSrc == HisDataSource.ZIPLINE) {
			if (freq == Bar.Frequency.MINUTE || freq == Bar.Frequency.MINUTE_1) {
				hisDataCsvP = rootP.resolve("minute").resolve(String.format("%s.csv", wc));
			} else if (freq == Bar.Frequency.DAY) {
				hisDataCsvP = rootP.resolve("daily").resolve(String.format("%s.csv", wc));
			} else {
				logger.warn("Unsupported Frequency {} for zipline hisdata !", freq);
				return Pair.of(false, null);
			}
		} else {
			hisDataCsvP = rootP.resolve(barFreqToHisDataDir(freq)).resolve(theDate.format(Constants.DATE_FORMAT_YYYY)).
					resolve(theDate.format(Constants.DATE_FORMAT_YYYYMMDD)).resolve(String.format("%s.csv", wc));
		}
		return Pair.of(hisDataCsvP.toFile().exists(), hisDataCsvP);
	}

	/**
	 * 读取 tushare 历史 Tick csv
	 * 
	 * @param wc wc
	 * @param theDate theDate
	 * @param tickCsvBaseP default: TS_TICK_CSV_BASE_DEFAULT
	 * @param csvFileNameExtP default: ".csv"
	 * @param csvFileEncP default: "UTF-8"
	 * @return 若 Tick csv 不存在, 返回 null.
	 * @throws IOException IOException
	 */
	public static Table readFromTicksCsvTs(String wc, LocalDate theDate, Optional<Path> tickCsvBaseP, Optional<String> csvFileNameExtP,
			Optional<String> csvFileEncP) throws IOException {

		Path tickCsvBase = tickCsvBaseP.orElse(TS_TICK_CSV_BASE_DEFAULT);
		String csvFileNameExt = csvFileNameExtP.orElse(".csv");
		String csvFileEnc = csvFileEncP.orElse("UTF-8");

		return readFromTicksCsvTs(wc, theDate, tickCsvBase, csvFileNameExt, csvFileEnc);
	}

	/**
	 * 读取 tushare 历史 Tick csv
	 * 
	 * @param wc wc
	 * @param theDate theDate
	 * @param tickCsvBase tickCsvBase
	 * @param csvFileNameExt csvFileNameExt
	 * @param csvFileEnc csvFileEnc
	 * @return 若 Tick csv 不存在, 返回 null.
	 * @throws IOException IOException
	 */
	public static Table readFromTicksCsvTs(String wc, LocalDate theDate, Path tickCsvBase, String csvFileNameExt,
			String csvFileEnc) throws IOException {

		String dtStr = theDate.format(Constants.DATE_FORMAT_YYYYMMDD);
		String dtYearStr = theDate.format(Constants.DATE_FORMAT_YYYY);
		Path tickCsvP = tickCsvBase.resolve(dtYearStr).resolve(dtStr).resolve(wc + csvFileNameExt);
		Table df = null;
		if (tickCsvP.toFile().exists()) {
			try (InputStreamReader reader = new InputStreamReader(new FileInputStream(tickCsvP.toFile()),
			        Charset.forName(csvFileEnc))) {
				// @formatter:off
				Map<String, ColumnType> colsTypeMap = MapUtils.putAll(new HashMap<>(), new Object[][] {
					{ Constants.TIME, ColumnType.LOCAL_TIME },
					{ Constants.PRICE, ColumnType.DOUBLE },
					{ Constants.CHANGE, ColumnType.DOUBLE },
					{ Constants.VOLUME, ColumnType.DOUBLE },
					{ Constants.AMOUNT, ColumnType.DOUBLE },
					{ "type", ColumnType.STRING } // optional field
				});
				ColumnType[] types = CommonUtils.genColumnsInfoFor(tickCsvP, colsTypeMap, csvFileEnc);
				// @formatter:on
				CsvReadOptions options = CsvReadOptions.builder(reader).missingValueIndicator("").columnTypes(types)
				        .timeFormat(Constants.TIME_FORMAT_STD).build();

				df = Table.read().usingOptions(options);
			}
			if (df.isEmpty()) {
				logger.info("empty tushare tick data file: {} !", tickCsvP);
			}
		} else {
			logger.info("tushare tick data file: {} does NOT exists !", tickCsvP);
		}
		return df;
	}

	/**
	 * 读取 tushare 历史 minutes csv
	 * 
	 * @param wc wc
	 * @param theDate theDate
	 * @param freq freq
	 * @param minsCsvBaseP default: TS_HIS_DATA_BASE_DIR_DEFAULT
	 * @param csvFileNameExtP default: ".csv"
	 * @param csvFileEncP default: "UTF-8"
	 * @return 若 minutes csv 不存在, 返回 null.
	 * @throws IOException IOException
	 */
	public static Table readFromMinutesCsvTs(String wc, LocalDate theDate, Bar.Frequency freq, Optional<Path> minsCsvBaseP,
			Optional<String> csvFileNameExtP, Optional<String> csvFileEncP) throws IOException {

		Path minsCsvBase = minsCsvBaseP.orElse(TS_HIS_DATA_BASE_DIR_DEFAULT);
		String csvFileNameExt = csvFileNameExtP.orElse(".csv");
		String csvFileEnc = csvFileEncP.orElse("UTF-8");

		return readFromMinutesCsvTs(wc, theDate, freq, minsCsvBase, csvFileNameExt, csvFileEnc);
	}

	/**
	 * 读取 tushare 历史 minutes csv: 若指定 freq csv 不存在, 则读取 minute csv 并进行 resample:
	 * 
	 * @param wc wc
	 * @param theDate theDate
	 * @param freq freq
	 * @param minsCsvBase minsCsvBase
	 * @param csvFileNameExt csvFileNameExt
	 * @param csvFileEnc csvFileEnc
	 * @return 若 minutes csv 不存在, 返回 null.
	 * @throws IOException IOException
	 */
	public static Table readFromMinutesCsvTs(String wc, LocalDate theDate, Bar.Frequency freq, Path minsCsvBase,
			String csvFileNameExt, String csvFileEnc) throws IOException {

		String dtStr = theDate.format(Constants.DATE_FORMAT_YYYYMMDD);
		String dtYearStr = theDate.format(Constants.DATE_FORMAT_YYYY);
		Path min1CsvP = minsCsvBase.resolve(CommonDataUtils.barFreqToHisDataDir(Bar.Frequency.MINUTE))
				.resolve(dtYearStr).resolve(dtStr).resolve(wc + csvFileNameExt);
		Path minsCsvP = minsCsvBase.resolve(CommonDataUtils.barFreqToHisDataDir(freq))
				.resolve(dtYearStr).resolve(dtStr).resolve(wc + csvFileNameExt);
		Table df = null;
		if (minsCsvP.toFile().exists()) {
			try (InputStreamReader reader = new InputStreamReader(new FileInputStream(minsCsvP.toFile()),
			        Charset.forName(csvFileEnc))) {
				// @formatter:off
				Map<String, ColumnType> colsTypeMap = MapUtils.putAll(new HashMap<>(), new Object[][] {
					{ Constants.DATE_TIME, ColumnType.LOCAL_DATE_TIME },
					{ Constants.BAR_FIELD_OPEN, ColumnType.DOUBLE },
					{ Constants.BAR_FIELD_CLOSE, ColumnType.DOUBLE },
					{ Constants.BAR_FIELD_HIGH, ColumnType.DOUBLE },
					{ Constants.BAR_FIELD_LOW, ColumnType.DOUBLE },
					{ Constants.BAR_FIELD_VOLUME, ColumnType.DOUBLE },
					{ Constants.BAR_FIELD_AMOUNT, ColumnType.DOUBLE },
					{ Constants.CHANGE, ColumnType.DOUBLE },
					{ Constants.PCT_CHANGE, ColumnType.DOUBLE }
				});
				ColumnType[] types = CommonUtils.genColumnsInfoFor(minsCsvP, colsTypeMap, csvFileEnc);
				// @formatter:on
				CsvReadOptions options = CsvReadOptions.builder(reader).missingValueIndicator("").columnTypes(types)
				        .dateTimeFormat(Constants.DATE_TIME_FORMAT_STD).build();

				df = Table.read().usingOptions(options);
			}
			if (df.isEmpty()) {
				logger.info("empty tushare minutes({}) data file: {} .", freq, minsCsvP);
			}
		} else if (min1CsvP.toFile().exists()) {
			logger.debug("tushare minutes({}) data file: {} does NOT exists, resample from minute data file: {}", freq, minsCsvP, min1CsvP);
			LocalDateTime startDm = LocalDateTime.of(theDate, Constants.OPEN_TIME_AM);
			LocalDateTime endDm = LocalDateTime.of(theDate, Constants.CLOSE_TIME_PM);
			Table dfMin1;
			try (InputStreamReader reader = new InputStreamReader(new FileInputStream(min1CsvP.toFile()),
					Charset.forName(csvFileEnc))) {
				// @formatter:off
				Map<String, ColumnType> colsTypeMap = MapUtils.putAll(new HashMap<>(), new Object[][] {
						{ Constants.DATE_TIME, ColumnType.LOCAL_DATE_TIME },
						{ Constants.BAR_FIELD_OPEN, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_CLOSE, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_HIGH, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_LOW, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_VOLUME, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_AMOUNT, ColumnType.DOUBLE },
						{ Constants.CHANGE, ColumnType.DOUBLE },
						{ Constants.PCT_CHANGE, ColumnType.DOUBLE }
				});
				ColumnType[] types = CommonUtils.genColumnsInfoFor(min1CsvP, colsTypeMap, csvFileEnc);
				// @formatter:on
				CsvReadOptions options = CsvReadOptions.builder(reader).missingValueIndicator("").columnTypes(types)
						.dateTimeFormat(Constants.DATE_TIME_FORMAT_STD).build();

				dfMin1 = Table.read().usingOptions(options);
			}
			if (dfMin1.isEmpty()) {
				df = dfMin1;
				logger.info("empty tushare minute({}) data file: {} .", Bar.Frequency.MINUTE, min1CsvP);
			} else {
				df = resampleDataFrameMinuteToTs(dfMin1, startDm, endDm, freq);
			}
		} else {
			logger.info("tushare minutes({}) data file: {} does NOT exists !", freq, minsCsvP);
		}
		return df;
	}

	public static Table readMinutesCsvTs(String wc, LocalDate theDate, Bar.Frequency freq, Optional<Boolean> paddingEmptyBarsP, Optional<Boolean> preferTickP,
	                                     Optional<Path> minsCsvBaseP, Optional<Path> tickCsvBaseP,
	                                     Optional<String> csvFileNameExtP, Optional<String> csvFileEncP) throws IOException {

		return readMinutesCsvTs(wc, theDate, freq, paddingEmptyBarsP.orElse(false), preferTickP.orElse(false),
				minsCsvBaseP.orElse(TS_HIS_DATA_BASE_DIR_DEFAULT), tickCsvBaseP.orElse(TS_TICK_CSV_BASE_DEFAULT), csvFileNameExtP.orElse(".csv"),
				csvFileEncP.orElse("UTF-8"));
	}

	/**
	 *
	 * @param wc wc
	 * @param theDate theDate
	 * @param freq freq
	 * @param paddingEmptyBars paddingEmptyBars
	 * @param preferTick preferTick
	 * @param minsCsvBase minsCsvBase
	 * @param tickCsvBase tickCsvBase
	 * @param csvFileNameExt csvFileNameExt
	 * @param csvFileEnc csvFileEnc
	 * @return Table
	 * @throws IOException IOException
	 */
	public static Table readMinutesCsvTs(String wc, LocalDate theDate, Bar.Frequency freq, boolean paddingEmptyBars, boolean preferTick,
	                                     Path minsCsvBase, Path tickCsvBase, String csvFileNameExt, String csvFileEnc) throws IOException {

		// TODO:
		String dtStr = theDate.format(Constants.DATE_FORMAT_YYYYMMDD);
		String dtYearStr = theDate.format(Constants.DATE_FORMAT_YYYY);
		Path tickCsvP = tickCsvBase.resolve(dtYearStr).resolve(dtYearStr).resolve(dtStr).resolve(wc + csvFileNameExt);
		Path min1CsvP = minsCsvBase.resolve(CommonDataUtils.barFreqToHisDataDir(Bar.Frequency.MINUTE))
				.resolve(dtYearStr).resolve(dtStr).resolve(wc + csvFileNameExt);
		Path minsCsvP = minsCsvBase.resolve(CommonDataUtils.barFreqToHisDataDir(freq))
				.resolve(dtYearStr).resolve(dtStr).resolve(wc + csvFileNameExt);
		Table df = null;
		if (preferTick) {
			if (tickCsvP.toFile().exists()) {
				Table ticksDf = readFromTicksCsvTs(wc, theDate, tickCsvBase, csvFileNameExt, csvFileEnc);
			} else {
				logger.warn("tushare tick (preferred) data file: {} does NOT exists !", tickCsvP);
			}
		}
		if (minsCsvP.toFile().exists()) {  // 直接读取 minutes csv:
			try (InputStreamReader reader = new InputStreamReader(new FileInputStream(minsCsvP.toFile()),
					Charset.forName(csvFileEnc))) {
				// @formatter:off
				Map<String, ColumnType> colsTypeMap = MapUtils.putAll(new HashMap<>(), new Object[][] {
						{ Constants.DATE_TIME, ColumnType.LOCAL_DATE_TIME },
						{ Constants.BAR_FIELD_OPEN, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_CLOSE, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_HIGH, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_LOW, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_VOLUME, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_AMOUNT, ColumnType.DOUBLE },
						{ Constants.CHANGE, ColumnType.DOUBLE },
						{ Constants.PCT_CHANGE, ColumnType.DOUBLE }
				});
				ColumnType[] types = CommonUtils.genColumnsInfoFor(minsCsvP, colsTypeMap, csvFileEnc);
				// @formatter:on
				CsvReadOptions options = CsvReadOptions.builder(reader).missingValueIndicator("").columnTypes(types)
						.dateTimeFormat(Constants.DATE_TIME_FORMAT_STD).build();

				df = Table.read().usingOptions(options);
			}
			if (df.isEmpty()) {
				logger.info("empty tushare minutes({}) data file: {} .", freq, minsCsvP);
			}
		} else if (min1CsvP.toFile().exists()) {  // 先读取 min_1 csv, 再 resample:
			logger.debug("tushare minutes({}) data file: {} does NOT exists, resample from minute data file: {}", freq, minsCsvP, min1CsvP);
			LocalDateTime startDm = LocalDateTime.of(theDate, Constants.OPEN_TIME_AM);
			LocalDateTime endDm = LocalDateTime.of(theDate, Constants.CLOSE_TIME_PM);
			Table dfMin1;
			try (InputStreamReader reader = new InputStreamReader(new FileInputStream(min1CsvP.toFile()),
					Charset.forName(csvFileEnc))) {
				// @formatter:off
				Map<String, ColumnType> colsTypeMap = MapUtils.putAll(new HashMap<>(), new Object[][] {
						{ Constants.DATE_TIME, ColumnType.LOCAL_DATE_TIME },
						{ Constants.BAR_FIELD_OPEN, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_CLOSE, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_HIGH, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_LOW, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_VOLUME, ColumnType.DOUBLE },
						{ Constants.BAR_FIELD_AMOUNT, ColumnType.DOUBLE },
						{ Constants.CHANGE, ColumnType.DOUBLE },
						{ Constants.PCT_CHANGE, ColumnType.DOUBLE }
				});
				ColumnType[] types = CommonUtils.genColumnsInfoFor(min1CsvP, colsTypeMap, csvFileEnc);
				// @formatter:on
				CsvReadOptions options = CsvReadOptions.builder(reader).missingValueIndicator("").columnTypes(types)
						.dateTimeFormat(Constants.DATE_TIME_FORMAT_STD).build();

				dfMin1 = Table.read().usingOptions(options);
			}
			if (dfMin1.isEmpty()) {
				df = dfMin1;
				logger.info("empty tushare minute({}) data file: {} .", Bar.Frequency.MINUTE, min1CsvP);
			} else {
				df = resampleDataFrameMinuteToTs(dfMin1, startDm, endDm, freq);
			}
		} else {
			logger.info("tushare minutes({}) data file: {} does NOT exists !", freq, minsCsvP);
		}
		if (paddingEmptyBars) {
			df = paddingEmptyMinutesBarsTs(df, wc, theDate, freq, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
		}
		return df;
	}

	/*
	 * 1. TsData -> DataFrame (Tick): qy.qyalgotrader.utils.Helper#tsDataToDataFrame();
	 * 2. DataFrame (Tick) -(sample)-> DataFrame (min): qy.qyalgotrader.utils.CommonDataUtils#sampleDataFrameTicksToMinuteTs();
	 * 3. DataFrame (min) -(resample)-> DataFrame (min_n): qy.qyalgotrader.utils.CommonDataUtils#resampleDataFrameMinuteToTs();
	 * 4. DataFrame (min_n) -> List<Bar>: qy.qyalgotrader.utils.CommonDataUtils#minutesDataFrameToBarListTs().
	 */
	/**
	 * Ticks 合成 minute Bar:
	 *
	 * @param codeOrWc codeOrWc
	 * @param td - 所属交易日
	 * @param ticksDf ticksDf
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param paddingEmptyBars paddingEmptyBars
	 * @return Table
	 */
	public static Table sampleDataFrameTicksToMinuteTs(String codeOrWc, LocalDate td, Table ticksDf,
	        LocalDateTime startDm, LocalDateTime endDm, boolean paddingEmptyBars) {

		return sampleDataFrameTicksToMinuteTs(codeOrWc, td, ticksDf, startDm, endDm, paddingEmptyBars, (int) Constants.LAST_TICK_DELAY_SEC_DEFAULT);
	}

	/**
	 * Ticks 合成 minute Bar:
	 * 
	 * @param codeOrWc codeOrWc
	 * @param td - 所属交易日
	 * @param ticksDf ticksDf
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param paddingEmptyBars paddingEmptyBars
	 * @param lstTkDelay default: Constants.LAST_TICK_DELAY_SEC_DEFAULT
	 * @return Table
	 */
	public static Table sampleDataFrameTicksToMinuteTs(String codeOrWc, LocalDate td, Table ticksDf,
	        LocalDateTime startDm, LocalDateTime endDm, boolean paddingEmptyBars, int lstTkDelay) {

		// minute 级别周期 df:
		List<Column<?>> smplCols = new ArrayList<>();
		DateTimeColumn dmCol = DateTimeColumn.create(Constants.DATE_TIME);
		dmCol.setPrintFormatter(Constants.DATE_TIME_FORMAT_STD);
		smplCols.add(dmCol);
		for (String e : Constants.TS_DEFAULT_FIELDS_MIN) {
			DoubleColumn doubleCol = DoubleColumn.create(e);
			if (e.equals(Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("volume", "resp"))
			        || e.equals(Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("amt", "resp"))) {
				// 避免使用科学计数格式:
				NumberFormat fmt = NumberFormat.getNumberInstance();
				fmt.setGroupingUsed(false);
				doubleCol.setPrintFormatter(fmt, "");
			}
			smplCols.add(doubleCol);
		}
		Table smplDf = Table.create(smplCols);

		if (ticksDf == null || ticksDf.isEmpty()) {
			return smplDf;
		}

		// 用来计算 bar 的 涨跌 及 涨跌幅, 每新产生一个 bar 后更新, 初始值为前收盘价 (从 tickDf 中获取):
		double priceRefVal = ticksDf.row(0).getDouble(Constants.PRICE) - ticksDf.row(0).getDouble(Constants.CHANGE);
		double lastBarClose = priceRefVal;
		LocalDateTime sliceStartDm = startDm;
		while (sliceStartDm.compareTo(endDm) < 0) {
			LocalDateTime sliceEndDm = sliceStartDm.plusSeconds(Bar.Frequency.MINUTE.getValue());

			// 连续竞价收盘 (交易暂停) 最后一个 Bar (若 sliceEndDm > endDm, 则亦不会丢失最后一个 Tick):
			Pair<Boolean, LocalDateTime> lastBarTup = CommonDataUtils.isSseSzseContiaLastTimeRange(codeOrWc, sliceStartDm, sliceEndDm,
					OptionalInt.of(lstTkDelay), Optional.of(td), Optional.empty());
			if (!lastBarTup.getLeft()) {
				// 跳过非交易时间:
				if (CommonDataUtils.isIllegalSseSzseTickDatetime(codeOrWc, sliceStartDm,
						OptionalInt.of(0), Optional.empty(), Optional.empty())) {
					sliceStartDm = sliceEndDm;
					continue;
				}
			}

			Selection filter = null;
			if (sliceStartDm.toLocalTime().compareTo(Constants.OPEN_TIME_AM) == 0) {
				// 连续竞价时段首个 Bar:
				filter = ticksDf.timeColumn(Constants.TIME).isOnOrAfter(Constants.OPEN_TIME_AM_CALL_AUCTION)
						.and(ticksDf.timeColumn(Constants.TIME).isBefore(sliceEndDm.toLocalTime()));
			} else if (lastBarTup.getLeft()) {
				// 连续竞价收盘 (交易暂停) 最后一个 Bar:
				filter = ticksDf.timeColumn(Constants.TIME).isOnOrAfter(sliceStartDm.toLocalTime())
						.and(ticksDf.timeColumn(Constants.TIME).isBefore(lastBarTup.getRight().toLocalTime()));
			} else {
				filter = ticksDf.timeColumn(Constants.TIME).isOnOrAfter(sliceStartDm.toLocalTime())
						.and(ticksDf.timeColumn(Constants.TIME).isBefore(sliceEndDm.toLocalTime()));
			}

			Table ticksSlice = ticksDf.where(filter);

			if (!ticksSlice.isEmpty()) {
				double open = ticksSlice.doubleColumn(Constants.PRICE).getDouble(0);
				double high = ticksSlice.doubleColumn(Constants.PRICE).max();
				double low = ticksSlice.doubleColumn(Constants.PRICE).min();
				double close = ticksSlice.doubleColumn(Constants.PRICE).getDouble(ticksSlice.rowCount() - 1);
				// 涨跌:
				double priceChg = close - priceRefVal;
				// 涨跌幅:
				double priceChgPct = !DoubleMath.fuzzyEquals(priceRefVal, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)
				        ? priceChg / priceRefVal
				        : Double.NaN;
				// 成交量:
				double volume = ticksSlice.doubleColumn(Constants.VOLUME).sum();
				// 补 成交额:
				double amount = ticksSlice.doubleColumn(Constants.AMOUNT).sum();
				// 更新最近收盘价:
				priceRefVal = close;
				// 当成交量为 0 时 (仅盘口发生变化, 有 Tick), 不生成 Bar:
				if (DoubleMath.fuzzyCompare(volume, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) > 0) {
					Row row = smplDf.appendRow();
					row.setDateTime(Constants.DATE_TIME, sliceStartDm);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("open", "resp"), open);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("close", "resp"), close);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("high", "resp"), high);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("low", "resp"), low);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("volume", "resp"), volume);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("amt", "resp"), amount);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("chg", "resp"), priceChg);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("pct_chg", "resp"), priceChgPct);
					// 更新 lastBarClose:
					lastBarClose = close;
				}
			}
			sliceStartDm = sliceEndDm;
		}

		if (paddingEmptyBars) {
			smplDf = paddingEmptyMinutesBarsTs(smplDf, codeOrWc, td, Bar.Frequency.MINUTE,
					Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
		}
		return smplDf;
	}

	/**
	 * 
	 * @param min1Df min1Df
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param resFreq resFreq
	 * @return Table
	 */
	public static Table resampleDataFrameMinuteToTs(Table min1Df, LocalDateTime startDm, LocalDateTime endDm,
			Bar.Frequency resFreq) {

		return resampleDataFrameMinuteToTs(min1Df, startDm, endDm, resFreq, Double.NaN);
	}

	/**
	 * minute 级别 变频 为 resFreq 级别:
	 * 
	 * @param min1Df min1Df
	 * @param startDm startDm
	 * @param endDm endDm
	 * @param resFreq resFreq
	 * @param priceRefVal 上一个 Bar 收盘价; default: Double.NaN
	 * @return Table
	 */
	public static Table resampleDataFrameMinuteToTs(Table min1Df, LocalDateTime startDm, LocalDateTime endDm,
			Bar.Frequency resFreq, double priceRefVal) {

		// 高级别 minute 周期 df:
		List<Column<?>> rsmpCols = new ArrayList<>();
		DateTimeColumn dmCol = DateTimeColumn.create(Constants.DATE_TIME);
		dmCol.setPrintFormatter(Constants.DATE_TIME_FORMAT_STD);
		rsmpCols.add(dmCol);
		for (String e : Constants.TS_DEFAULT_FIELDS_MIN) {
			DoubleColumn doubleCol = DoubleColumn.create(e);
			if (e.equals(Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("volume", "resp"))
			        || e.equals(Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("amt", "resp"))) {
				// 避免使用科学计数格式:
				NumberFormat fmt = NumberFormat.getNumberInstance();
				fmt.setGroupingUsed(false);
				doubleCol.setPrintFormatter(fmt, "");
			}
			rsmpCols.add(doubleCol);
		}
		Table rsmpDf = Table.create(rsmpCols);

		if (min1Df == null || min1Df.isEmpty()) {
			return rsmpDf;
		}

		if (Double.isNaN(priceRefVal)) {
			priceRefVal = min1Df.row(0).getDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("close", "resp"))
					- min1Df.row(0).getDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("chg", "resp"));
		}

		LocalDateTime sliceStartDm = startDm;
		while (sliceStartDm.compareTo(endDm) < 0) {
			LocalDateTime sliceEndDm = sliceStartDm.plusSeconds(resFreq.getValue());
			// TimeRange 范围内所有 Bars:
			Selection filter = min1Df.dateTimeColumn(Constants.DATE_TIME).isOnOrAfter(sliceStartDm)
					.and(min1Df.dateTimeColumn(Constants.DATE_TIME).isBefore(sliceEndDm));
			Table barsSlice = min1Df.where(filter);

			if (!barsSlice.isEmpty()) {
				double open = barsSlice.doubleColumn((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("open", "resp")).getDouble(0);
				double high = barsSlice.doubleColumn((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("high", "resp")).max();
				double low = barsSlice.doubleColumn((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("low", "resp")).min();
				double close = barsSlice.doubleColumn((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("close", "resp"))
						.getDouble(barsSlice.rowCount() - 1);
				double volume = barsSlice.doubleColumn((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("volume", "resp")).sum();
				double amount = barsSlice.doubleColumn((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("amt", "resp")).sum();
				// 涨跌
				double priceChg = close - priceRefVal;
				// 涨跌幅
				double priceChgPct = !DoubleMath.fuzzyEquals(priceRefVal, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)
						? priceChg / priceRefVal
						: Double.NaN;

				// 更新最近收盘价:
				priceRefVal = close;

				if (volume > 0) {
					// 当成交量为 0 时 (仅盘口发生变化, 有 Tick), 不生成 Bar:
					Row row = rsmpDf.appendRow();
					row.setDateTime(Constants.DATE_TIME, sliceStartDm);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("open", "resp"), open);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("close", "resp"), close);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("high", "resp"), high);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("low", "resp"), low);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("volume", "resp"), volume);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("amt", "resp"), amount);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("chg", "resp"), priceChg);
					row.setDouble((String) Constants.WIND_FIELD_NAME_MAPPING_MIN_1.get("pct_chg", "resp"), priceChgPct);
				}
			}

			sliceStartDm = sliceEndDm;
		}

		return rsmpDf;
	}

	/**
	 *
	 * @param minsDf minsDf
	 * @param wc wc
	 * @param theDate theDate
	 * @param freq freq
	 * @param openTimeAmP openTimeAmP
	 * @param closeTimeAmP closeTimeAmP
	 * @param openTimePmP openTimePmP
	 * @param closeTimePmP closeTimePmP
	 * @return Table
	 */
	public static Table paddingEmptyMinutesBarsTs(Table minsDf, String wc, LocalDate theDate, Bar.Frequency freq,
	                                              Optional<LocalTime> openTimeAmP, Optional<LocalTime> closeTimeAmP,
	                                              Optional<LocalTime> openTimePmP, Optional<LocalTime> closeTimePmP) {

		Map<String, Object> tdInfos = getTradeInfos(wc, theDate);
		return paddingEmptyMinutesBarsTs(minsDf, wc, theDate, freq, openTimeAmP.orElse((LocalTime) tdInfos.get("openTimeAm")),
				closeTimeAmP.orElse((LocalTime) tdInfos.get("closeTimeAm")),
				openTimePmP.orElse((LocalTime) tdInfos.get("openTimePm")),
				closeTimePmP.orElse((LocalTime) tdInfos.get("closeTimePm")));
	}

	/**
	 *
	 * @param minsDf minsDf
	 * @param wc wc
	 * @param theDate theDate
	 * @param freq freq
	 * @param openTimeAm openTimeAm
	 * @param closeTimeAm closeTimeAm
	 * @param openTimePm openTimePm
	 * @param closeTimePm closeTimePm
	 * @return Table
	 */
	public static Table paddingEmptyMinutesBarsTs(Table minsDf, String wc, LocalDate theDate, Bar.Frequency freq,
	                                              LocalTime openTimeAm, LocalTime closeTimeAm,
	                                              LocalTime openTimePm, LocalTime closeTimePm) {

		if (minsDf == null || minsDf.isEmpty()) {
			logger.info("minsDf <{}, {}> is null or empty !", wc, theDate);
			return minsDf;
		}
		Table padded = CommonUtils.makeTableWithSameColumns(minsDf);  // new Table
		List<String> columns = Arrays.asList("open", "close", "high", "low", "volume", "amount", "change", "pctchange");
		Pair<LocalTime, Map<String, Double>> lastBar = Pair.of(minsDf.row(0).getDateTime(Constants.DATE_TIME).toLocalTime(),
				CommonUtils.tableRow2MapDouble(minsDf.row(0), columns));
		int rowIdx = 0;
		if (lastBar.getLeft().compareTo(openTimeAm) != 0) {  // 缺开盘首个 Bar:
			Map<String, Double> barInfo = lastBar.getRight();
			double preClose = barInfo.get("close") - barInfo.get("change");
			lastBar = Pair.of(openTimeAm, MapUtils.putAll(new HashMap<>(), new Object[][] {
					{"open", preClose},
					{"close", preClose},
					{"high", preClose},
					{"low", preClose},
					{"volume", 0.0},
					{"amount", 0.0},
					{"change", 0.0},
					{"pctchange", 0.0},
			}));
		} else {
			rowIdx++;
		}
		// row: 0
		Row r = padded.appendRow();
		r.setDateTime(Constants.DATE_TIME, LocalDateTime.of(theDate, lastBar.getLeft()));
		r.setDouble("open", lastBar.getRight().get("open"));
		r.setDouble("close", lastBar.getRight().get("close"));
		r.setDouble("high", lastBar.getRight().get("high"));
		r.setDouble("low", lastBar.getRight().get("low"));
		r.setDouble("volume", lastBar.getRight().get("volume"));
		r.setDouble("amount", lastBar.getRight().get("amount"));
		r.setDouble("change", lastBar.getRight().get("change"));
		r.setDouble("pctchange", lastBar.getRight().get("pctchange"));
		// end of row: 0
		Pair<LocalTime, LocalTime> currRange = Pair.of(lastBar.getLeft().plusSeconds(freq.getValue()),
				lastBar.getLeft().plusSeconds(freq.getValue()).plusSeconds(freq.getValue()));
		while (currRange.getLeft().compareTo(closeTimePm) < 0) {  // for each bar TimeRange
			// 跳过中午休市时间段:
			if (!(currRange.getLeft().compareTo(closeTimeAm) >= 0 && currRange.getRight().compareTo(openTimePm) <= 0)) {
				boolean useLastBar = false;
				if (rowIdx < minsDf.rowCount()) {
					LocalTime currBarTime = minsDf.row(rowIdx).getDateTime(Constants.DATE_TIME).toLocalTime();
					assert currBarTime.compareTo(currRange.getLeft()) >= 0;
					if (currBarTime.compareTo(currRange.getLeft()) == 0) {
						padded.addRow(rowIdx, minsDf);
						lastBar = Pair.of(currBarTime, CommonUtils.tableRow2MapDouble(minsDf.row(rowIdx), columns));
						rowIdx++;
					} else {
						useLastBar = true;
					}
				} else {
					useLastBar = true;
				}
				if (useLastBar) {
					Row nr = padded.appendRow();
					nr.setDateTime(Constants.DATE_TIME, LocalDateTime.of(theDate, currRange.getLeft()));
					nr.setDouble("open", lastBar.getRight().get("close"));
					nr.setDouble("close", lastBar.getRight().get("close"));
					nr.setDouble("high", lastBar.getRight().get("close"));
					nr.setDouble("low", lastBar.getRight().get("close"));
					nr.setDouble("volume", 0.0);
					nr.setDouble("amount", 0.0);
					nr.setDouble("change", 0.0);
					nr.setDouble("pctchange", 0.0);
					lastBar = Pair.of(currRange.getLeft(), CommonUtils.tableRow2MapDouble(nr, columns));
				}
			}
			// range 后移一格:
			currRange = Pair.of(currRange.getLeft().plusSeconds(freq.getValue()), currRange.getRight().plusSeconds(freq.getValue()));
		}
		return padded;
	}

	/**
	 *
	 * @param minsDf minsDf
	 * @param freq freq
	 * @param wc wc
	 * @param td td
	 * @return List&ly;Bar&gt;
	 * @throws Exception Exception
	 */
	public static List<Bar> minutesDataFrameToBarListTs(Table minsDf, Bar.Frequency freq,
			String wc, LocalDate td) throws Exception {

		List<Bar> barList = new ArrayList<>();

		Map<String, Object> tdInfos = CommonDataUtils.getTradeInfos(wc, td);
		Mean meanUtils = new Mean();
		double[] closes = minsDf.doubleColumn(Constants.BAR_FIELD_CLOSE).asDoubleArray();
		double[] vols = minsDf.doubleColumn(Constants.BAR_FIELD_VOLUME).asDoubleArray();
		double[] amts = minsDf.doubleColumn(Constants.BAR_FIELD_AMOUNT).asDoubleArray();
		for (int i = 0; i < minsDf.rowCount(); i++) {
			/*
			 * Bar 均价 (vwap) = amount / volume / tradeUnit:
			 *     * 沪深交易所指数: 不可用;
			 * 日内分时均价 (vwap_intraday):
			 *     * 沪深交易所指数: close 加权平均 (按 amount 加权);
			 */
			Row aRow = minsDf.row(i);
			ZonedDateTime dm = CommonDataUtils.toMarketDatetime(aRow.getDateTime(Constants.DATE_TIME));
			double open = aRow.getDouble(Constants.BAR_FIELD_OPEN);
			double high = aRow.getDouble(Constants.BAR_FIELD_HIGH);
			double low = aRow.getDouble(Constants.BAR_FIELD_LOW);
			double close = aRow.getDouble(Constants.BAR_FIELD_CLOSE);
			double vol = aRow.getDouble(Constants.BAR_FIELD_VOLUME);
			double amt = aRow.getDouble(Constants.BAR_FIELD_AMOUNT);
			// Bar VWAP = amount / volume / tradeUnit:
			double vwap = Double.NaN;
			double vwapIntraday = Double.NaN;
			if (!DoubleMath.fuzzyEquals(vol, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
				vwap = amt / vol / (double) tdInfos.get("tradeUnit");
			}
			if (CommonDataUtils.isSseSzseIndex(wc)) {
				// 沪深交易所指数 Bar vwap 和 vwap_intraday:
				// vwap: NaN
				// vwap_intraday: close 加权平均 (按 amount 加权):
				vwap = Double.NaN;
				vwapIntraday = meanUtils.evaluate(closes, amts, 0, i + 1);
			} else {
				double amtAccu = StatUtils.sum(amts, 0, i + 1);
				double volAccu = StatUtils.sum(vols, 0, i + 1);
				if (!DoubleMath.fuzzyEquals(volAccu, 0.0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {
					vwapIntraday = amtAccu / volAccu / (double) tdInfos.get("tradeUnit");
				}
			}
			// 仅 min_1 频率 Bar 计算 vwap_intraday:
			if (!(freq == Bar.Frequency.MINUTE || freq == Bar.Frequency.MINUTE_1)) {
				vwapIntraday = Double.NaN;
			}

			// @formatter:off
			Bar aBar = new BasicBar(dm, open, high, low, close, vol, close, freq,
			        MapUtils.putAll(new HashMap<>(), new Object[][] {
			        	{ Constants.BAR_FIELD_AMOUNT, amt },
			            { Constants.BAR_FIELD_VWAP, vwap },
			            { Constants.BAR_FIELD_VWAP_INTRADAY, vwapIntraday },
			        }));
			// @formatter:on
			barList.add(aBar);
		}

		if (barList.size() == 0) {
			logger.warn("EMPTY minutes DataFrame ({}) for {} !", freq, wc);
		}

		return barList;
	}

	/**
	 * 
	 */
	private CommonDataUtils() {

	}
}
